Java MVC 1.0 入门手册(四)
十、将 Java MVC 连接到数据库
如果您希望将数据保存更长时间,或者如果必须从不同的会话(不同的用户)持续访问数据,则需要数据库。本章涉及 SQL(结构化查询语言)数据库。关于介绍,参见例如在 https://en.wikipedia.org/wiki/Database 的文章。
JPA (Java 持久性 API)是用于从 Jakarta EE 内部访问关系数据库的专用技术。它的目的是提供 SQL 表和 Java 对象之间的桥梁。这项任务比其他基本数据方案复杂得多。其原因是,在关系数据库模式中,我们在不同的表之间有关联:一个表中的一行可能引用另一个表中的一行或多行,或者反过来,也可能有跨越三个或更多表的引用。想想列类型转换——与 Java 相比,数据库可能对数字、布尔指示符以及日期和时间有不同的想法。此外,如果在表引用中使用数据库表中的null值,以及在将它们转换为 Java 值时,需要更加注意。
在这一章中,我们将讨论在 Java MVC 中使用 JPA 的基本问题。要全面深入地了解 JPA,包括比本章更复杂的问题,请参考网站上的在线 JPA 文档和规范。好的首发网址是 https://docs.oracle.com/javaee/6/tutorial/doc/bnbpy.html 。
用 JPA 抽象出数据库访问
JPA 的主要目的之一是抽象出数据库访问,并将数据库对象映射到 Java 类。最终,我们希望能够查询数据库并获得 Java 对象,或者将 Java 对象放入数据库。JPA 隐藏了如何做到这一点的细节,包括用户名和密码等连接属性,还包括处理连接生命周期。
用于此目的的中央 JPA 类是EntityManager,它使用一个名为persistence.xml的配置文件,以及 Jakarta EE 应用服务器内部的一些设置。在 Java 端,对应于表行的类被称为实体类。JPA 的概述见图 10-1 。
图 10-1
Jakarta EE 工作中的 JPA
设置 SQL 数据库
SQL 数据库有两种类型——您可以拥有完全成熟的客户端-服务器数据库和嵌入式数据库(可能使用一些内存存储)。在本书中,我们使用 GlassFish 服务器发行版中包含的 Apache Derby 数据库。该数据库独立于 GlassFish 运行,但是 GlassFish 管理工具也提供了一些用于处理 Apache Derby 实例的命令。作为客户端,我们从 Java MVC 应用内部使用 JPA。
Note
在 GlassFish 文档中,您会经常看到名称“JavaDB”作为数据库的产品名称。JavaDB 实际上是 Derby 的名字,它包含在 JDK 版本 6 到 8 中。现在它有点过时了,所以我们在这本书里不用“JavaDB”这个名字。
切换到不同的数据库产品是一种非侵入性的操作,所以您可以从 Apache Derby 开始学习 JPA,以后再切换到其他数据库管理系统。
Note
从架构的角度来看,数据库访问最好包含在 EJB 或 EAR 模块中。为了简单起见,我们将 JPA 直接包含在 Java MVC 项目中,但是在 EJB 或 EAR 模块中这样做的过程非常相似。
如果启动 GlassFish 服务器,Apache Derby 数据库也不会自动启动。相反,您必须在控制台内部运行它:
cd [GLASSFISH_INST]
bin/asadmin start-database
其中[GLASSFISH_INST]是您的 GlassFish 服务器的安装目录。
Caution
虽然它们都由asadmin管理,但是 GlassFish Jakarta EE 服务器和 Apache Derby 数据库管理系统是相互独立的。如果您停止其中一个,另一个会继续运行。
要停止正在运行的 Apache Derby,请在控制台中输入以下命令:
cd [GLASSFISH_INST]
bin/asadmin stop-database
创建数据源
为了让 JPA 工作,我们需要向项目添加一个对象关系映射 (ORM)库。这里有几个选项,但是我们选择 EclipseLink 作为 ORM 库,因为 EclipseLink 是 JPA 2.2 的参考实现(这是 Jakarta EE 8 和 Java MVC 1.0 中使用的 JPA 版本)。
ORM 不直接连接到数据库,而是连接到抽象出数据库访问的数据源。这种间接方式允许使用服务器端管理工具建立连接池、缓存、事务性和数据处理管理。
为了为 GlassFish 创建合适的数据源,请在用于启动数据库的同一终端中输入以下内容:
cd [GLASSFISH_INST]
cd javadb/bin
# start the DB client
./ij
(或者对 Windows 使用ij。)我们现在在ij数据库客户端中,因为ij>提示符出现在终端中,所以您可以看到这一点。输入以下命令创建一个名为hello的数据库(在一行中输入,在create=前不要有空格):
ij> connect 'jdbc:derby://localhost:1527/hello;
create=true;user=user0';
现在创建的数据库拥有者名为user0。我们还为用户添加了一个密码:
ij> call SYSCS_UTIL.SYSCS_CREATE_USER('user0','pw715');
Note
默认情况下,Apache Derby 不支持新数据库的身份验证。如果您仅将数据库用于开发,这通常不会造成问题,因为网络访问仅限于本地用户。然而,许多 Java 应用和数据库工具,如果您试图在没有认证的情况下访问数据库,会表现得很奇怪,所以我们添加了一个密码。
接下来,重新启动数据库以使身份验证开始工作:
cd [GLASSFISH_INST]
cd bin
./asadmin stop-database
./asadmin start-database
这只需要做一次。退出并重新打开ij工具内的连接(或按 Ctrl+D 完全退出ij;然后重启ij并再次连接):
ij> disconnect;
ij> connect 'jdbc:derby://localhost:1527/hello;
user=user0;password=pw715';
(在一行中输入最后一个ij命令。)您可以检查身份验证机制:如果您忽略了用户名和/或密码,您将得到一条适当的错误消息。
为了透明和简单地连接到数据库,我们在 GlassFish 服务器配置中创建了两个资源:
cd [GLASSFISH_INST]
cd bin
./asadmin create-jdbc-connection-pool \
--datasourceclassname \
org.apache.derby.jdbc.ClientXADataSource \
--restype javax.sql.XADataSource \
--property \
portNumber=1527:password=pw715:user=user0:
serverName=localhost:databaseName=hello:
securityMechanism=3 \
HelloPool
./asadmin create-jdbc-resource \
--connectionpoolid HelloPool jdbc/Hello
(在user=user0:或databaseName = hello:之后没有换行和空格。)这将创建一个连接池和一个与之连接的 JDBC 资源。我们稍后将使用jdbc/Hello标识符来允许 JPA 连接到数据库。
如果您在http://localhost:4848进入 web 浏览器中的管理控制台,您可以看到这两个配置项目。导航到资源➤ JDBC ➤ JDBC 资源和资源➤ JDBC ➤ JDBC 连接池。见图 10-2 。
图 10-2
JDBC 资源公司
在本章的其余部分,我们假设您知道如何输入数据库命令。要么使用ij工具(启动后不要忘记连接),要么使用任何其他数据库客户端,例如名为 Squirrel 的开源工具。
准备会员注册申请
在这一章中,我们为 Java MVC 开发了一个基本的成员管理应用。成员存储在名为MEMBER的数据库表中。创建表和用于生成唯一 ID 的序列生成器的 SQL 命令如下:
CREATE TABLE MEMBER (
ID INT NOT NULL,
NAME VARCHAR(128) NOT NULL,
PRIMARY KEY (ID));
INSERT INTO MEMBER (ID, NAME)
VALUES (-3, 'John'),
(-2, 'Linda'),
(-1, 'Pat');
CREATE SEQUENCE MEMBER_SEQ start with 1 increment by 50;
我们还添加了几个示例条目。
Note
Apache Derby 知道如何自动生成惟一的 id。然而,我们让 EclipseLink 来处理这个问题。因此,ID字段是一个简单的整数值字段,没有任何额外的语义。EclipseLink 需要这个序列来生成这样的惟一 id(至少如果它是按照我们将要使用的方式使用的话)。
新数据库项目的项目结构如下:
Project HelloJpa
src/main/java
book.javamvc.jpa
data
User.java
db
Member.java
MemberDAO.java
i18n
BundleForEL.java
SetBundleFilter.java
model
UserEntering.java
UserList.java
AjaxController.java
App.java
HelloJpaController.java
RootRedirector.java
src/main/resources
book.javamvc.jpa.messages
Messages.properties
META-INF
persistence.xml
src/main/webapp
js
jquery-3.5.1.min.js
WEB-INF
views
index.jsp
beans.xml
glassfish-web.xml
build.gradle
gradle.properties
settings.gradle
我们不想混合 Java MVC 模型类和数据库模型类,所以在User.java类中,我们抽象出任何用户数据:
package book.javamvc.jpa.data;
public class User {
private int id;
private String name;
public User() {
}
public User(int id, String name) {
this.id = id;
this.name = name;
}
// Getters and setters...
}
BundleForEL和SetBundleFilter类与HelloWorld应用中的完全相同,但是增加了因子分解配置值(在其中一个练习中完成)。为了清楚起见,我在这里重复代码:
package book.javamvc.jpa.i18n;
import java.util.Enumeration;
import java.util.Locale;
import java.util.ResourceBundle;
import javax.servlet.http.HttpServletRequest;
public class BundleForEL extends ResourceBundle {
private BundleForEL(Locale locale, String baseName) {
setLocale(locale, baseName);
}
public static void setFor(HttpServletRequest request,
String i18nAttributeName, String i18nBaseName) {
if (request.getSession().
getAttribute(i18nAttributeName) == null) {
request.getSession().setAttribute(
i18nAttributeName,
new BundleForEL(request.getLocale(),
i18nBaseName));
}
}
public void setLocale(Locale locale,
String baseName) {
if (parent == null ||
!parent.getLocale().equals(locale)) {
setParent(getBundle(baseName, locale));
}
}
@Override
public Enumeration<String> getKeys() {
return parent.getKeys();
}
@Override
protected Object handleGetObject(String key) {
return parent.getObject(key);
}
}
和
package book.javamvc.jpa.i18n;
import java.io.IOException;
import java.util.Map;
import javax.inject.Inject;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.Application;
@WebFilter("/*")
public class SetBundleFilter implements Filter {
@Inject private Application appl;
private String i18nAttributeName;
private String i18nBaseName;
@Override
public void init(FilterConfig filterConfig)
throws ServletException {
Map<String,Object> applProps = appl.getProperties();
i18nAttributeName = (String) applProps.get(
"I18N_TEXT_ATTRIBUTE_NAME");
i18nBaseName = (String) applProps.get(
"I18N_TEXT_BASE_NAME");
}
@Override
public void doFilter(ServletRequest request,
ServletResponse response, FilterChain chain)
throws IOException, ServletException {
BundleForEL.setFor((HttpServletRequest) request,
i18nAttributeName, i18nBaseName);
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
}
我们将用于新成员条目和成员列表的两个 Java MVC 模型类放在book.javamvc.jpa.model包中。代码内容如下:
package book.javamvc.jpa.model;
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;
import book.javamvc.jpa.data.User;
@Named
@RequestScoped
public class UserEntering extends User {
}
和
package book.javamvc.jpa.model;
import java.util.ArrayList;
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;
import book.javamvc.jpa.data.User;
@Named
@RequestScoped
public class UserList extends ArrayList<User>{
private static final long serialVersionUID =
8570272213112459191L;
}
App和RootRedirector类与HelloWorld应用中的相同,但是在其中一个练习中进行了重构:
package book.javamvc.jpa;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.PostConstruct; import javax.ws.rs.ApplicationPath; import javax.ws.rs.core.Application;
@ApplicationPath("/mvc")
public class App extends Application {
@PostConstruct
public void init() {
}
@Override
public Map<String, Object> getProperties() {
Map<String, Object> res = new HashMap<>();
res.put("I18N_TEXT_ATTRIBUTE_NAME",
"msg");
res.put("I18N_TEXT_BASE_NAME",
"book.javamvc.jpa.messages.Messages");
return res;
}
}
和
package book.javamvc.jpa;
import javax.servlet.FilterChain;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Redirecting http://localhost:8080/HelloJpa/
* This way we don't need a <welcome-file-list> in web.xml
*/
@WebFilter(urlPatterns = "/")
public class RootRedirector extends HttpFilter {
private static final long serialVersionUID =
7332909156163673868L;
@Override
protected void doFilter(final HttpServletRequest req,
final HttpServletResponse res,
final FilterChain chain) throws IOException {
res.sendRedirect("mvc/hello");
}
}
build.gradle采用以下代码:
plugins {
id 'war'
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
repositories {
jcenter()
}
dependencies {
testImplementation 'junit:junit:4.12'
implementation 'javax:javaee-api:8.0'
implementation 'javax.mvc:javax.mvc-api:1.0.0'
implementation 'org.eclipse.krazo:krazo-jersey:1.1.0-M1'
implementation 'jstl:jstl:1.2'
implementation 'org.eclipse.persistence:'+
'eclipselink:2.7.7'
}
task localDeploy(dependsOn: war,
description:">>> Local deploy task") {
// Take the code from the HelloWorld example
}
task localUndeploy(
description:">>> Local undeploy task") {
// Take the code from the HelloWorld example
}
settings.gradle文件由项目生成器向导准备,gradle.properties文件可以直接从第四章中取出。
所有其他文件将在后续章节中介绍。
将 EclipseLink 添加为 ORM
要将 EclipseLink ORM 添加到项目中,请将以下内容添加到build.gradle文件的dependencies { }部分:
dependencies {
...
implementation 'org.eclipse.persistence:'+
'eclipselink:2.7.7'
}
接下来,创建一个包含以下内容的src/main/resources/META-INF/persistence.xml文件:
<persistence
xmlns:=
"http://java.sun.com/xml/ns/persistence"
xmlns:xsi=
"http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation=
"http://java.sun.com/xml/ns/persistence persistence_1_0.xsd"
version="1.0">
<persistence-unit name="default"
transaction-type="JTA">
<jta-data-source>jdbc/Hello</jta-data-source>
<exclude-unlisted-classes>
false
</exclude-unlisted-classes>
<properties />
</persistence-unit>
</persistence>
这是 JPA 的中央配置文件。在这里,我们指出如何连接到数据库。请注意,我们引用了之前配置的数据源资源。
Note
Eclipse IDE 有几个用于 JPA 相关开发的助手向导,它还有一个 JPA 方面,您可以将它添加到项目中。我决定不在这一介绍性章节中使用它们,以避免供应商锁定,并展示遵循 JPA 规范所需的基础知识。您可以免费尝试 Eclipse 的 JPA 方面。
控制器
成员注册应用的控制器与前几章中的HelloWorld控制器非常相似——我们也有一个登录页面,这次列出了所有成员,还有一个新成员的输入表单。添加成员会导致数据库INSERT操作,与HelloWorld不同,我们不会显示响应页面,而是用更新后的成员列表重新加载索引页面。代码内容如下:
package book.javamvc.jpa;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import javax.ejb.EJB;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.inject.Named;
import javax.mvc.Controller;
import javax.mvc.binding.BindingResult;
import javax.mvc.binding.MvcBinding;
import javax.mvc.binding.ParamError;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
import book.javamvc.jpa.data.User;
import book.javamvc.jpa.db.MemberDAO;
import book.javamvc.jpa.model.UserEntering;
import book.javamvc.jpa.model.UserList;
@Path("/hello")
@Controller
public class HelloJpaController {
@Named
@RequestScoped
public static class ErrorMessages {
private List<String> msgs = new ArrayList<>();
public List<String> getMsgs() {
return msgs;
}
public void setMsgs(List<String> msgs) {
this.msgs = msgs;
}
public void addMessage(String msg) {
msgs.add(msg);
}
}
@Inject private ErrorMessages errorMessages;
@Inject private BindingResult br;
@Inject private UserEntering userEntering;
@Inject private UserList userList;
@EJB private MemberDAO memberDao;
@GET
public String showIndex() {
addUserList();
return "index.jsp";
}
@POST
@Path("/add")
public Response addMember(
@MvcBinding @FormParam("name") String name) {
if(br.isFailed()) {
br.getAllErrors().stream().
forEach((ParamError pe) -> {
errorMessages.addMessage(pe.getParamName() +
": " + pe.getMessage());
});
}
userEntering.setName(name);
memberDao.addMember(userEntering.getName());
addUserList();
return Response.ok("index.jsp").build();
}
////////////////////////////////////////////////////////
////////////////////////////////////////////////////////
private void addUserList() {
userList.addAll(
memberDao.allMembers().stream().map(member -> {
return new User(member.getId(),
member.getName());
}).collect(Collectors.toList())
);
}
}
HelloWorld示例应用中的一个重要区别是包含了用于数据库操作的MemberDAO数据访问对象,它从成员添加和列表方法中被引用。我们将在接下来的章节中讨论道。
成员删除由 AJAX 请求处理。与我们在前几章所做的相反,我们没有让 Java MVC 控制器处理 AJAX 请求。相反,我们添加了一个额外的 JAX-RS 控制器,如下所示:
just for AJAX:
package book.javamvc.jpa;
import javax.ejb.EJB;
import javax.ws.rs.DELETE;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.core.Response;
import book.javamvc.jpa.db.MemberDAO;
@Path("/ajax")
public class AjaxController {
@EJB private MemberDAO memberDao;
@DELETE
@Path("/delete/{id}")
public Response delete(@PathParam("id") int id) {
memberDao.deleteMember(id);
return Response.ok("{}").build();
}
}
添加数据访问对象
数据访问对象(DAO)是一个 Java 类,它封装了像 CRUD(创建、读取、更新和删除)这样的数据库操作。然后,DAO 的客户端不必知道DAO 是如何工作的,而只需要关注业务功能。
在控制器内部,名为MemberDAO的 DAO 类通过@EJB注释注入。这个班去book.javamvc.jpa.db包。创建包和类,然后编写以下类代码:
package book.javamvc.jpa.db;
import java.util.List;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
@Stateless
public class MemberDAO {
@PersistenceContext
private EntityManager em;
public int addMember(String name) {
List<?> l = em.createQuery(
"SELECT m FROM Member m WHERE m.name=:name").
setParameter("name", name).
getResultList();
int id = 0;
if(l.isEmpty()) {
Member member = new Member();
member.setName(name);
em.persist(member);
em.flush(); // needed to get the ID
id = member.getId();
} else {
id = ((Member)l.get(0)).getId();
}
return id;
}
public List<Member> allMembers() {
TypedQuery<Member> q = em.createQuery(
"SELECT m FROM Member m", Member.class);
List<Member> l = q.getResultList();
return l;
}
public void deleteMember(int id) {
Member member = em.find(Member.class, id);
em.remove(member);
}
}
我们提供了添加成员(避免重复)、列出所有成员和删除成员的方法。更新和搜索方法留待将来改进。您可以看到数据库操作是由一个EntityManager独占处理的,它是由@PersistenceContext注释注入的。通过配置文件persistence.xml,JPA 知道实体管理器需要访问哪个数据库。对于当前需要的大多数操作,我们可以使用来自EntityManager类的方法。唯一的例外是我们使用 JPA 查询语言表达式SELECT m FROM Member m的完整列表。
应用通过@Stateless类注释知道这个 DAO 是一个 EJB。因此,容器(服务器中处理 EJB 对象的部分)知道这个类的实例没有状态。
更新视图
对于基本会员注册申请,作为一个视图,我们只需要index.jsp文件:
<%@ page contentType="text/html;charset=UTF-8"
language="java" %>
<%@ taglib prefix="c"
uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt"
uri="http://java.sun.com/jsp/jstl/fmt" %>
<html>
<head>
<meta charset="UTF-8">
<script type="text/javascript"
src="${mvc.basePath}/../js/jquery-3.5.1.min.js">
</script>
<title>${msg.title}</title>
<script type="text/javascript">
function deleteItm(id) {
var url =
"${pageContext.servletContext.contextPath}" +
"/mvc/ajax/delete/" + id;
jQuery.ajax({
url : url,
method: "DELETE",
dataType: 'json',
success: function(data, textStatus, jqXHR) {
jQuery('#itm-'+id).remove();
},
error: function (jqXHR, textStatus,
errorThrown) {
console.log(errorThrown);
}
});
return false;
}
</script>
</head>
<body>
<form method="post"
action="${mvc.uriBuilder(
'HelloJpaController#greeting').build()}">
${msg.enter_name}
<input type="text" name="name" />
<input type="submit" value="${msg.btn_submit}" />
</form>
<table>
<thead>
<tr>
<th>${msg.tblhdr_id}</th>
<th>${msg.tblhdr_name}</th>
<th></th>
</tr>
<thead>
<tbody>
<c:forEach items="${userList}" var="itm">
<tr id="itm-${itm.id}">
<td>${itm.id}</td>
<td>${itm.name}</td>
<td><button onclick="deleteItm(${itm.id})">
${msg.btn_delete}</button></td>
</tr>
</c:forEach>
</tbody>
</table>
</body>
</html>
此页面显示了用于输入新成员和完整成员列表的表单。由于我们添加到每个表格行的itm-[ID],删除一个条目的 AJAX 代码可以删除一个表格行,而不必重新加载整个页面。
该视图引用 jQuery 库。下载下来复制到src/main/webapp/js。相应地调整版本。
一个语言资源到src/main/resources/book/javamvc/jpa/messages/Messages.properties:
title = Hello Jpa
enter_name = Enter your name:
btn_delete = Delete
btn_submit = Submit
tblhdr_id = ID
tblhdr_name = Name
可以从第四章复制beans.xml和glassfish-web.xml文件。
添加实体
实体是表行作为对象的表示。如果我们想到MEMBER表,那么实体就是有名称和单一 ID 的东西。显然,这对应于带有name和id字段的 Java 类。所以我们创建了这样一个类,并把它放在book.javamvc.jpa.db包中:
public class Member {
private int id; // + getter/setter
private String name; // + getter/setter
}
为了完成数据库接口过程,我们需要添加元信息。这是一个实体类的信息、表名、列名、专用 ID 列名、唯一 ID 生成器规范和数据库字段值约束。和 Java 通常的情况一样,我们对这种元信息使用注释。我们这一类,加上所有这些修正,内容如下:
package book.javamvc.jpa.db;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import javax.validation.constraints.NotNull;
@Entity
@Table(name="MEMBER")
@SequenceGenerator(name="HELLO_SEQ",
initialValue=1, allocationSize = 50)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY,
generator = "HELLO_SEQ")
@Column(name = "id")
private int id;
@NotNull
@Column(name = "name")
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
具体来说,我们添加的注释是:
-
@Entity:将它标记为一个实体,这样 JPA 就知道这是一个实体类。 -
@Table:用于指定表格名称。如果省略,类名(不带包)将用作表名。 -
@SequenceGenerator:用于指定唯一 id 的序列生成器。 -
@Id:表示对应的字段是实体的唯一标识。 -
@GeneratedValue:表示新实体将自动为该字段生成值。 -
@Column:用于指定该字段对应的列名。如果未指定,字段名将用作列名。 -
@NotNull:表示字段和数据库字段都不能是null的约束。
给定实体类,JPA 现在知道如何将数据库条目字段映射到 Java 类。通过调整 Java MVC 控制器和添加 DAO 和实体类,应用拥有了全功能的 JPA 支持,您可以在http://localhost:8080/HelloJpa进行部署和尝试。此外,尝试重新启动服务器,并验证这些条目是否被持久化并在服务器重新启动后仍然存在。您还可以使用数据库客户端工具直接检查数据库,并研究添加到那里的表行。
添加关系
关系数据是关于关系的,就像一个表条目引用其他表的条目一样。JPA 为这种关系提供了一种解决方案,同样是通过可以添加到实体类中的特殊注释。
考虑下面的例子:在我们的成员资格应用中,我们添加了另一个名为STATUS的表,其中包含成员资格条目,比如 Gold、Platinum、Senior 或任何您可能想到的条目。每个成员可能有0到N状态条目,所以我们谈论成员和状态条目之间的“一对多”关系。
为了实现这一点,我们首先创建一个STATUS表和一个STATUS_SEQ序列:
CREATE TABLE STATUS (
ID INT NOT NULL,
MEMBER_ID INT NOT NULL,
NAME VARCHAR(128) NOT NULL,
PRIMARY KEY (ID));
CREATE SEQUENCE STATUS_SEQ start with 1 increment by 50;
接下来,我们在book.javamvc.jpa.db包中创建一个名为Status的新实体类,其内容如下:
package book.jakarta8.calypsojpa.jpa;
import javax.persistence.*;
import javax.validation.constraints.*;
@Entity
@Table(name="STATUS")
@SequenceGenerator(name="STATUS_SEQ",
initialValue=1, allocationSize = 50)
public class Status implements Comparable<Status> {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY,
generator="STATUS_SEQ")
@Column(name = "ID")
private int id;
@NotNull
@Column(name = "MEMBER_ID")
private int memberId;
@NotNull
@Column(name = "NAME")
private String name;
public Status() {
}
public Status(String name) {
this.name = name;
}
@Override
public int compareTo(Status o) {
return -o.name.compareTo(name);
}
// + getters and setters
}
我们添加了一个构造函数,使用名称。重要的是要知道 JPA 规范要求有一个公共的无参数构造函数。
在实体类Member中,我们添加了一个对应于成员和状态之间实际关系的字段:
...
@JoinColumn(name = "MEMBER_ID")
@OneToMany(cascade = CascadeType.ALL, orphanRemoval= true)
private Set<Status> status; // + getter / setters
...
其他的都没动过。@JoinColumn字段引用了与相关联的类或表中的成员,所以我们不必为这个新字段更新成员表。
因为两个实体类的关系是通过@OneToMany宣布的,任何实体管理器操作都会自动将数据库操作正确级联到相关实体。例如,要创建新成员,您可以编写以下内容:
...
Member m = new Member();
m.setName(...);
Set<Status> status = new HashSet<>();
status.add(new Status("Platinum"));
status.add(new Status("Priority"));
m.setStatus(status);
em.persist(m);
...
所以你不必明确地告诉实体管理器持久化相关的Status实体。
在前端代码中,您可以添加一个以逗号分隔的状态值列表的文本字段,或者一个选择列表框或菜单来反映这种关系。这同样适用于UPDATE和DELETE操作。由于@OneToMany注释中的cascade = CascadeType.ALL,如果成员被删除,JPA 甚至会从STATUS表中删除相关的Status条目。
关系数据模型中还有其他关联类型。您可以在 JPA 中为实体声明的可能关联类型如下:
- 对于实体类
A的实体,存在零到多个实体类B的相关条目。在类A中,用OneToMany注释定义了一个Set类型的字段。在实体B的表中,有一个名为ID_A(或任何您喜欢的名称)的外键,在实体类B中有一个名为aId(或任何您喜欢的名称)的字段,指向Aid。为了告诉A它是如何与B相关联的,然后添加另一个名为@JoinColumn的注释,如下所示:
@OneToMany
@JoinColumn(name="ID_A") // In table B!
private Set<B> b;
或者给@OneToMany添加一个属性,如下所示:
-
@曼顿
对于实体类
A,的零个或多个实体,存在实体类B的一个相关条目。在类A中,您添加了一个带有@ManyToOne和@JoinColumn注释的B类型的字段,对于后者,您为连接提供了一个列名(在A的表中):
@OneToMany(mappedBy = "aId") // Field in class B!
private Set<B> b;
-
@OneToOne
对于实体类
A的一个实体,存在实体类B的一个相关条目。在类A中,您添加了一个带有@OneToOne和@JoinColumn注释的B类型的字段,对于后者,您为连接提供了一个列名(在A的表中):
@ManyToOne
@JoinColumn(name="ID_B") // In table A
private B b;
-
@ManyToMany
对于实体类
A的零个或多个实体,存在实体类B的零个或多个相关条目。这里,我们需要第三个表作为中间连接表;比如MTM_A_B,带柱ID_A和ID_B。实体类A(具有 ID 列"ID")中的注释如下所示:
@OneToOne
@JoinColumn(name="ID_B") // In table A
private B b;
@ManyToMany
@JoinTable(
name = "MTM_A_B",
joinColumns = @JoinColumn(
name = "ID_A",
referencedColumnName="ID"),
inverseJoinColumns = @JoinColumn(
name = "ID_B",
referencedColumnName="ID"))
private Set<B> b;
练习
-
练习 5: 将
STATUS表添加到数据库中,并更新成员条目应用的代码以反映成员的状态。为简单起见,使用一个文本字段,您可以在其中输入以逗号分隔的状态值列表。 -
练习 6: 说出 JPA 中用于表间关系的四种注释。
-
一个实体类对应一个数据库表。
-
实体类必须与数据库表同名。
-
实体类的属性(字段)必须与数据库表中的列具有相同的名称。
-
实体类的属性可以有限制。
- 练习 4: 以下哪些是正确的?
-
Dao 需要通过 JPA 连接到数据库。
-
需要 Dao 来提供数据库用户名和密码。
-
在 DAOs 中,必须指定数据库列名。
-
Dao 用于避免在 JPA 客户端类中使用数据库表细节。
-
要使用 Dao,它们必须作为 EJB 注入。
-
**练习 2:**JPA 的哪个组件(或者概念,如果你喜欢的话)在数据库表和 Java 对象(三个字母的缩写)之间转换?
-
练习 3: 以下哪一项是正确的:
-
JPA 通过一些数据源连接到数据库,数据源是服务器管理的资源。
-
JPA 通过一些数据源连接到数据库,这些数据源是 JPA 自己提供的。
-
JPA 通过 JDBC 连接到一个数据库。
-
JPA 通过 EJB 连接到一个数据库。
- 练习 1: 以下哪些是正确的?
摘要
JPA (Java 持久性 API)是用于从 Jakarta EE 内部访问关系数据库的专用技术。它的目的是提供 SQL 表和 Java 对象之间的桥梁。
JPA 的主要目的之一是抽象出数据库访问,并将数据库对象映射到 Java 类。最终,我们希望能够查询数据库并获得 Java 对象,或者将 Java 对象放入数据库。JPA 有助于隐藏如何做到这一点的细节,包括用户名和密码等连接属性,以及处理连接生命周期。
用于此目的的核心 JPA 类是EntityManager类,它使用一个名为persistence.xml的配置文件,以及 Jakarta EE 应用服务器内部的一些设置。在 Java 端,对应于表行的类被称为实体类。
为了让 JPA 工作,我们需要向项目添加一个对象关系映射 (ORM)库。这里有几个选项,但是我们选择 EclipseLink 作为 ORM 库,因为 EclipseLink 是 JPA 2.2 的参考实现(这是 Jakarta EE 8 和 Java MVC 1.0 中使用的 JPA 版本)。
ORM 不直接连接到数据库,而是连接到抽象出数据库访问的数据源。这种间接方式允许使用服务器端管理工具建立连接池、缓存、事务性和数据处理管理。数据源以特定于服务器产品的方式安装。
数据访问对象(DAO)是一个 Java 类,它封装了像 CRUD(创建、读取、更新和删除)这样的数据库操作。然后,DAO 的客户端不必知道DAO 是如何工作的,而只需要关注业务功能。
实体是表行作为对象的表示。为了完成数据库接口过程,我们需要添加元信息。这是一个实体类的信息、表名、列名、专用 ID 列名、唯一 ID 生成器规范和数据库字段值约束。和 Java 通常的情况一样,我们对这种元信息使用注释。
给定实体类,JPA 现在知道如何将数据库条目字段映射到 Java 类。通过调整 Java MVC 控制器和添加 DAO 和实体类,应用拥有了全功能的 JPA 支持。
关系数据是关于关系的,比如一个表条目引用其他表的条目。JPA 为这种关系提供了一种解决方案,同样是通过可以添加到实体类中的特殊注释。
您可以在 JPA 中为实体声明的可能关联类型如下:
- 对于实体类
A的实体,存在零到多个实体类B的相关条目。在类A中,用OneToMany注释定义了一个Set类型的字段。在实体B的表中,有一个名为ID_A(或任何您喜欢的名称)的外键,在实体类B中有一个指向Aid 的aId字段(或任何您喜欢的名称)。为了告诉A它是如何与B相关联的,然后添加另一个名为@JoinColumn的注释,如下所示:
@OneToMany
@JoinColumn(name="ID_A") // In table B!
private Set<B> b;
或者给@OneToMany添加一个属性,如下所示:
-
@曼顿
对于实体类
A的零个或多个实体,存在实体类B的一个相关条目。在类A中,您添加一个带有@ManyToOne和@JoinColumn注释的B类型的字段,对于后者,您为连接提供一个列名(在A的表中):
@OneToMany(mappedBy = "aId") // Field in class B!
private Set<B> b;
-
@OneToOne
对于实体类
A的一个实体,存在实体类B的一个相关条目。在类A中,您添加了一个带有@OneToOne和@JoinColumn注释的B类型的字段,对于后者,您为连接提供了一个列名(在A的表中):
@ManyToOne
@JoinColumn(name="ID_B") // In table A
private B b;
-
@ManyToMany
对于实体类
A的零个或多个实体,存在实体类B的零个或多个相关条目。这里,我们需要第三个表作为中间连接表;比如MTM_A_B,有ID_A和ID_B两列。实体类A(具有 ID 列"ID")中的注释如下所示:
@OneToOne
@JoinColumn(name="ID_B") // In table A
private B b;
@ManyToMany
@JoinTable(
name = "MTM_A_B",
joinColumns = @JoinColumn(
name = "ID_A",
referencedColumnName="ID"),
inverseJoinColumns = @JoinColumn(
name = "ID_B",
referencedColumnName="ID"))
private Set<B> b;
在下一章,我们将讨论 Java MVC 中的日志记录。****
十一、Java MVC 应用的日志记录
日志记录是任何中级到高级复杂性应用的重要组成部分。当程序通过它的执行路径运行时,几个日志语句描述程序正在做什么,哪些参数被传递给方法调用,局部变量和类字段有什么值以及它们如何改变,作出了哪些决定,等等。这些日志记录信息被收集并发送到文件、数据库、消息队列或其他地方,开发人员和操作团队可以调查程序流,以便修复错误或进行审计。
这一章是关于在你的程序中添加日志或者调查现有的服务器日志的各种选项。
系统流
Jakarta EE 构建其服务器技术所基于的 Java 标准环境(JSE)提供了众所周知的标准输出和错误输出流,如下所示:
System.out.println("Some information: ...");
System.err.println("Some error: ...");
虽然乍一看,使用这些流生成诊断信息似乎很容易,但不建议您使用此过程。主要原因是该方法高度依赖于操作系统和服务器产品。我们将很快介绍高级方法,但是如果您暂时想使用系统流进行诊断输出,那么了解大多数 Jakarta EE 服务器获取流并将它们重定向到某个文件是很重要的。
Note
到目前为止,我们使用输出和错误输出流进行诊断输出。我们这样做是为了简单。在任何严肃的项目中,您都不应该这样做,后续部分将向您展示如何避免这种情况。
Jakarta EE 8 GlassFish 服务器版本 5.1 将输出和错误输出流添加到您可以在
GLASSFISH_INST/glassfish/domains/domain1/logs
在这个通常冗长的清单中,您会将System.out和System.err输出识别为包含[SEVERE](用于System.err)和[INFO](用于System.out)的行:
...
[2019-05-20T14:42:03.791+0200] [glassfish 5.1] [SEVERE]
[] [] [tid: _ThreadID=28 _ThreadName=Thread-9]
[timeMillis: 1558356123791] [levelValue: 1000] [[
The System.err message ]]
...
[2019-05-20T14:42:03.796+0200] [glassfish 5.1] [INFO]
[NCLS-CORE-00022] [javax.enterprise.system.core]
[tid: _ThreadID=28
_ThreadName=RunLevelControllerThread-1558356114688]
[timeMillis: 1558356123796] [levelValue: 800] [[
The System.out message ]]
...
我们稍后将了解如何更改这些日志行的详细级别和格式。
GlassFish 中的 JDK 日志
日志 API 规范 JSR 47 是 Java 的一部分,可以被任何 Java 程序使用,包括 Jakarta EE 服务器应用,当然还有 Java MVC 程序。您可以从 https://jcp.org/en/jsr/detail?id=47 下载该规范。
GlassFish 日志文件
GlassFish 使用这个平台标准 API JSR 47 进行日志记录。除非您更改配置,否则您可以在以下位置找到日志文件
GLASSFISH_INST/glassfish/domains/domain1/logs/server.log
在同一个文件夹中,您还会找到名为server.log_TS的归档日志,其中TS是一个时间戳,比如2019-05-08T15-45-58。
标准日志格式被定义为各种信息片段的组合,当然包括实际的日志消息:
[Timestamp] [Product-ID]
[Message-Type] [Message-ID] [Logger-Name] [Thread-ID]
[Raw-Timestamp] [Log-Level]
[[Message]]
例如:
[2019-05-20T14:42:03.796+0200] [glassfish 5.1] [INFO]
[NCLS-CORE-00022] [javax.enterprise.system.core]
[tid: _ThreadID=28
_ThreadName=RunLevelControllerThread-1558356114688]
[timeMillis: 1558356123796]
[levelValue: 800]
[[Loading application xmlProcessing done in 742 ms]]
将日志记录输出添加到控制台
如果您希望日志记录输出也出现在启动 GlassFish 服务器的终端中,请使用以下命令:
cd GLASSFISH_INST
bin/asadmin start-domain --verbose
这将显示完整的日志输出。它也不会像没有–verbose的asadmin start-domain那样将服务器进程放在后台,所以当您关闭终端时,服务器将会停止。服务器启动后,您将无法在终端中输入更多命令(对于新命令,您当然可以输入第二个终端)。若要停止前台服务器进程,请按 Ctrl+C。
为您自己的项目使用标准日志 API
要使用 JSR 47 方法将诊断输出添加到您自己的类中,您可以在您的类中编写如下内容:
...
import java.util.logging.Logger;
public class MyClass {
private final static Logger LOG =
Logger.getLogger(MyClass.class.toString());
public void someMethod() {
LOG.entering(this.getClass().toString(),"someMethod");
...
// different logging levels:
LOG.finest("Finest: ...");
LOG.finer("Finer: ...");
LOG.fine("Fine: ...");
LOG.info("Some info: ...");
LOG.warning("Some warning: ...");
LOG.severe("Severe: ...");
...
LOG.exiting(this.getClass().toString(),"someMethod");
}
...
}
对于LOG.entering(),还有一个变体,您可以向日志记录语句添加方法参数。同样,对于LOG.exiting(),变量允许您向日志记录语句添加返回值:
...
public String someMethod(String p1, int p2) {
LOG.entering(this.getClass().toString(),"someMethod",
new Object[]{ p1, p2 });
...
String res = ...;
LOG.exiting(this.getClass().toString(),"someMethod",
res);
return res;
}
...
}
日志记录级别
从这些示例中,您可以看到可以使用几个级别来指示日志记录输出的严重性。对于标准测井,级别依次为严重 ➤ 警告 ➤ 信息 ➤ 精细 ➤ 精细 ➤ 精细。这大大提高了日志的可用性。在项目的早期阶段,您可以将日志记录阈值设置为一个较低的值,例如fine,您将在日志文件中看到所有fine级别的日志记录以及所有更高级别的日志记录,直到severe。
如果降低阈值(例如降低到finest,日志记录会显示更多细节,但是日志记录文件当然会更大。这就是为什么你这样做是为了修复 bug 拥有更多细节有助于您更容易地识别有问题的代码。在项目的后期,当成熟度上升时,您应用一个更高的阈值(例如warning)。这样,日志文件不会变得太大,但是您仍然可以在日志中看到重要的问题。
称为entering()和exiting()的特殊Logger方法属于日志级finer。我们在这里展示的所有其他方法都与同名级别匹配,因此 a LOG.severe()属于级别severe,a LOG.warning()属于级别warning,以此类推。
记录器层次结构和阈值
如果您创建如下所示的记录器:
Logger.getLogger("com.example.projxyz.domain.Person");
你可以跨越一个层级com ➤ com.example ➤ com.example.projxyz ➤ com.example.projxyz.domain ➤ com.example.projxyz.domain.Person
如果您分配了日志记录阈值,这将发挥作用。这种分配通过asadmin在配置中进行,或者在 web 管理控制台中进行。我们将很快看到如何做到这一点。知道阈值设置遵循记录器层次结构是很重要的。如果您给com分配一个级别LEV1(严重、警告、信息等),这意味着com处的完整子树获得了LEV1阈值。,除非您还为层次结构中更深层次的元素指定了级别。因此,如果您还为com.example分配了一个LEV2级别,那么对于com.example和该层次中更深层次的所有元素来说,LEV2优先于LEV1。更准确地说,规则如表 11-1 所示。
表 11-1
日志记录层次结构规则
|等级制度
|
水平
|
记录器
|
描述
|
| --- | --- | --- | --- |
| com | FINE | com.ClassA | FINE适用,因为com.ClassA在com层次结构中。 |
| com | FINE | org.ClassA | FINE不适用,因为org.ClassA不在com层级内。 |
| com.ClassA | FINER | com.ClassA | FINER适用,因为com.ClassA在com.ClassA层次结构中。FINE不再适用,因为层次规范com.ClassA比com更具体。 |
| com.example | WARNING | com.ClassA | WARNING不适用,因为com.ClassA不在com.example层级内。 |
| com.example | WARNING | com.example. ClassA | WARNING适用,因为com.example.ClassA在com.example层次结构中。为com指定的级别不再适用,因为com.example比com更具体。 |
| com.example | WARNING | org.example. ClassA | WARNING不适用,因为org。不在com.example的层级内。 |
日志记录配置
JSR 47 标准日志的日志配置依赖于名为logging.properties的配置文件。通常,该文件位于 JDK 安装目录中,但是 GlassFish 服务器会忽略标准日志记录配置,而是使用该文件:
GLASSFISH_INST/glassfish/domains/domain1/
config/logging.properties
这里指定了各种日志记录属性。我们不会全部讨论它们——JSR 47 的规范和 GlassFish 服务器文档会给你更多的想法。最重要的设置是级别阈值。你会在#All log level details线下找到它们:
...
#All log level details
com.sun.enterprise.server.logging.GFFileHandler.level=ALL
javax.enterprise.system.tools.admin.level=INFO
org.apache.jasper.level=INFO
javax.enterprise.system.core.level=INFO
javax.enterprise.system.core.classloading.level=INFO
java.util.logging.ConsoleHandler.level=FINEST
javax.enterprise.system.tools.deployment.level=INFO
javax.enterprise.system.core.transaction.level=INFO
org.apache.catalina.level=INFO
org.apache.coyote.level=INFO
javax.level=INFO
...
这里,我们已经有了一个分层级别分配的例子:如果您将级别从javax.enterprise.system.core.level更改为FINE,任何javax.记录器都将使用阈值INFO,因为javax.level = INFO行,但是javax.enterprise.system.core.Main记录器将使用FINE,因为它与我们刚刚输入的级别相匹配,并且更加具体。
稍后在logging.properties文件中设置形式.level=INFO可确保所有未在日志记录属性中指定的日志记录程序都将使用INFO阈值。这就是为什么在 GlassFish 的标准配置变体中,没有出现fine、finer或finest消息。
除了更改文件,您还可以在http://localhost:4848使用 web 管理控制台。导航至配置➤服务器-配置➤记录器设置。更改将直接写入logging.properties文件。
作为更改日志配置的第三种方式,asadmin命令行实用程序为我们提供了各种与日志相关的子命令。以下是一些例子:
./asadmin list-log-levels
# -> A list of all log levels, like
# javax <INFO>
# javax.mail <INFO>
# javax.org.glassfish.persistence <INFO>
# org.apache.catalina <INFO>
# org.apache.coyote <INFO>
# org.apache.jasper <INFO>
# ...
./asadmin delete-log-levels javax.mail
# -> Deletes a level specification
./asadmin set-log-levels javax.mail=WARNING
# -> Setting a specific log level
./asadmin list-log-attributes
# -> Shows all log attributes (not the levels)
./asadmin set-log-attributes \
com.sun.enterprise.server.logging.
GFFileHandler.rotationLimitInBytes=2000000
# (discard the line break after "logging.")
# -> Sets an attribute. Attribute names are the same
# as in the logging.properties file
./asadmin rotate-log
# -> Manually rotates the log file. Takes the current
# server.log file, archives it and starts a fresh
# empty server.log file.
日志记录级别的更改是动态的,因此您可以在服务器运行时更改日志记录级别。
日志记录格式
对于 JSR 47 标准日志记录,日志记录格式由日志记录处理程序规定。为了改变日志格式,您必须开发一个新的日志处理程序。这并不难实现,但是如果您需要改变格式并希望坚持 Java 平台日志记录,我们会让您自行决定。
否则,您可以很容易地切换到使用日志库。这种选择的大多数候选者都允许您通过调整配置属性来更改日志记录格式。我们将很快讨论 Log4j 日志框架,并讨论 Log4j 提供的日志格式化选项。
对其他服务器使用 JDK 标准日志记录
尽管大多数开发人员更喜欢使用日志库,如 Apache Commons Logging、Log4j 或 Logback,但是您也可以为 GlassFish 以外的服务器使用 JSR 47 日志。只要确保你提供了一个定制的logging.properties文件。但是,不要更改 JDK 安装文件夹中的logging.properties文件——不鼓励在那里更改配置。
相反,提供您自己的logging.properties文件,并将以下内容添加到服务器启动参数中(在一行中,删除换行符和=后的空格):
-Djava.util.logging.config.file=
/path/to/logging.properties
您的服务器文档将告诉您如何做到这一点。
向应用添加 Log4j 日志记录
Log4j 是一个日志框架,常用于各种 Java 应用。其特点包括:
-
API 和实现的清晰分离。在服务器环境中,您在服务器上安装 Log4j 实现,而在客户机上,您只需要引用一个小内存 Log4j API 库。
-
高性能。Log4j 包含 lambda 支持,因此如果相应的日志级别不被记录,就可以避免消息计算。例如,在
LOG.info("Error", () -> expensiveOperation())中,如果记录器的info级消息被禁用,方法调用将不会发生。 -
自动重新加载配置。对于 Log4j,很容易启用自动配置重载。日志记录配置中的任何更改都将立即生效,无需重新启动服务器。
-
可以在配置中设置日志记录格式和各种其他日志记录属性。
-
Log4 配置文件可以格式化为 XML、Java 属性、JSON 和 YAML。
-
Log4j 很容易被插件扩展。
Log4j 可以从 http://logging.apache.org/log4j/2.x/ 下载。仍然广泛使用的 Log4j 1.x 版本已被弃用,我们不会在本书中讨论 1 . x 版本中的 Log4j。
Log4j 需要一些额外的权限来通过安全检查。为此,请打开此文件:
GLASSFISH_INST/glassfish/domains/domain1/
config/server.policy
并在结尾添加以下内容:
// Added for Log4j2
grant {
permission
java.lang.reflect.ReflectPermission
"suppressAccessChecks";
permission
javax.management.MBeanServerPermission "*";
permission
javax.management.MBeanPermission "*", "*";
permission
java.lang.RuntimePermission "getenv.*";
};
Caution
这个要求是特定于 GlassFish 服务器的。对于其他服务器,可能需要不同的设置。
在服务器范围内添加 Log4j
在服务器范围内添加 Log4j 意味着将 Log4j 实现放在一个公共库文件夹中,编写一个 Log4j 配置文件,该文件同时服务于该服务器上运行的所有 Jakarta EE 应用,并让所有应用和应用模块使用 Log4j API。该设置只需配置一次,然后服务器上所有当前和未来的应用都可以轻松地使用 Log4 进行日志记录。因为简单,所以这种包含 Log4j 的方式可能是最常用的。相反,您可以在每个应用的基础上添加 Log4j,但是只有当您有重要的理由将 Log4j 封装到应用中时,才应该这样做,例如,如果您还在运行使用旧 Log4j 1.x 版本的遗留应用。我们稍后会描述这个方法。
要在服务器范围内添加 Log4j,首先要从 https://logging.apache.org/log4j/2.x/ 下载 Log4j 发行版。然后将log4j-core-2.11.2.jar、log4j-api-2.11.2.jar和log4j-appserver-2.11.2文件(或您下载的任何版本)复制到以下文件夹:
GLASSFISH_INST/glassfish/domains/domain1/
modules/autostart
Note
Log4j JAR 文件被实现为 OSGi 包。这就是我们将它们放入modules文件夹的原因。如果你不了解 OSGi,可以把它看作是一个先进的图书馆管理框架。
然后在GLASSFISH_INST/glassfish/domains/domain1/lib/classes文件夹中添加一个名为log4j2.json的文件。作为该文件的基本内容,请使用:
{
"configuration": {
"name": "Default",
"appenders": {
"RollingFile": {
"name":"File",
"fileName":
"${sys:com.sun.aas.instanceRoot}/logs/log4j.log",
"filePattern":
"${sys:com.sun.aas.instanceRoot}/
logs/log4j-backup-%d{MM-dd-yy-HH-mm-ss}-%i.gz",
"PatternLayout": {
"pattern":
"%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n"
},
"Policies": {
"SizeBasedTriggeringPolicy": {
"size":"10 MB"
}
},
"DefaultRolloverStrategy": {
"max":"10"
}
}
},
"loggers": {
"logger" : [
{
"name" : "book.javamvc",
"level":"debug",
"appender-ref": {
"ref":"File"
}
},{
"name" : "some.other.logger",
"level":"info",
"appender-ref": {
"ref":"File"
}
}
],
"root": {
"level":"error",
"appender-ref": {
"ref":"File"
}
}
}
}
}
这添加了一个具有error级别的根日志记录器和另外两个日志记录器,称为book.javamvc和some.other.logger,阈值级别分别设置为debug和info。“logger”数组中的记录器名称对应于记录器层次结构规范。它们的工作方式与标准 JDK 测井过程(JSR 47)中描述的方式相同。因此book.javamvc记录器适用于book.javamvc.SomeClass和book.javamvc.pckg.OtherClass的记录语句,但不适用于book.jakarta99.FooClass。特殊的“根”记录器是默认的,它匹配所有没有明确记录器规范的记录器。
这个文件为您提供了一个起点。您可以添加更多的附加器和记录器。请参阅 Internet 上最新的 Log4j2 文档,了解如何扩展配置。
Note
Log4j 允许配置文件使用不同的格式。我们选择 JSON 格式是因为它的简洁性。
如果服务器正在运行,请重新启动它。因为以这种方式添加 Log4j 的全局性质,所以需要这样做。现在可以开始在应用中使用 Log4j 了,如“在编码中使用 Log4j”一节所述。
Note
添加-Dlog4j2.debug作为服务器启动 JVM 参数,以获得关于 Log4j 正在做什么的更多输出。该元诊断信息被打印到标准server.log文件中。
更改日志记录格式
在 Log4j 配置文件中,我们已经指定了一个日志记录模式:
...
"pattern":
"%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n"
...
这将打印一个由%d{yyyy-MM-dd HH:mm:ss}指定的时间戳,由%p指定的日志级别(5向输出添加一个填充符),由%c{1}指定的日志名称的最后一个路径元素,由%L指定的行号,以及由%m指定的消息。%n最后在末尾加了一个换行符。
这个你可以随意改。在线 Log4j2 手册中标题为“布局”的部分列出了所有选项。表 11-2 显示了最重要的选项。
表 11-2
记录模式
|模式
|
描述
|
| --- | --- |
| m | 消息。 |
| C | 记录器的名称。 |
| c[N] | 只有记录器名称的最后一个N路径部分。因此,对于名为org.example.memory.Main的记录器,一个%c{1}创建Main作为输出,一个%{2}创建memory.Main,以此类推。 |
| c[-N] | 删除记录器名称的第一个N路径部分。所以用一个名为org.example.memory.Main的记录器,一个%c{-1}创建example.memory.Main,以此类推。 |
| c[1.] | 将记录器名称中除最后一部分以外的所有部分替换为点“.”。因此,通过一个名为org.example.memory.Main的记录器,一个%c{1.}创建了o.e.m.Main。 |
| p | 日志级别。 |
| -5p | 日志级别,用空格右填充到五个字符。 |
| d | 输出类似2019-09-23的时间戳07:23:45,123。 |
| d[DEFAULT_MICROS] | 与普通%d相同,但增加了微秒:2019-09-23 07:23:45,123456。 |
| d[ISO8601] | 输出如2019-09-23T07:23:45,123。 |
| d[UNIX_MILLIS] | 自 1970-01-01 00:00:00 UTC 以来的毫秒数。 |
| highlight{p} | 将 ANSI 颜色添加到封闭图案p中。比如:highlight{%d %-5p %c{1.}: %m}%n。 |
| L | 行号。这是一个昂贵的操作;小心使用。 |
| M | 方法名称。这是一个昂贵的操作;小心使用。 |
| n | 换行。 |
| t | 线程的名称。 |
| T | 线程的 ID。 |
Log4j2 还以 CSV 格式、GELF 格式、嵌入在 HTML 页面中以及 JSON、XML 或 YAML 格式创建日志输出。详情参见 Log4j2 手册。
将 Log4j 添加到 Jakarta EE Web 应用
如果您认为应该在每个应用的基础上添加 Log4j,并且让服务器上运行的其他应用不受影响,那么您可以将 Log4j 实现添加到您的 web 应用(WAR)中。
Note
如果您的服务器还运行使用旧 Log4j 1.x 的遗留应用,那么以这种隔离方式运行 Log4j 可能是必要的。
为了添加 Log4j 实现,我们更新了 Gradle 构建文件中的依赖项。打开build.gradle文件,将以下内容添加到dependencies { }部分:
implementation 'org.apache.logging.log4j:log4j-core
:2.11.2'
implementation 'com.fasterxml.jackson.core:jackson-core
:2.7.4'
implementation 'com.fasterxml.jackson.core:jackson-
databind:2.7.4'
implementation 'com.fasterxml.jackson.core:jackson-
annotations:2.7.4'
这里,中心部分是对log4j-core的依赖;需要依赖于jackson,因为我们将使用 JSON 格式的配置文件,而 Log4j 需要jackson来解析它们。
Log4j 配置文件需要被称为log4j2.json,对于 web 应用(WARs),它必须放在src/main/resources文件夹中。作为一个简单的配置,将log4j2.json的内容设置如下:
{
"configuration": {
"name": "Default",
"appenders": {
"RollingFile": {
"name":"File",
"fileName":
"${sys:com.sun.aas.instanceRoot}/logs/log4j.log",
"filePattern":
"${sys:com.sun.aas.instanceRoot}/
logs/log4j-backup-%d{MM-dd-yy-HH-mm-ss}-%i.gz",
"PatternLayout": {
"pattern":
"%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n"
},
"Policies": {
"SizeBasedTriggeringPolicy": {
"size":"10 MB"
}
},
"DefaultRolloverStrategy": {
"max":"10"
}
}
},
"loggers": {
"logger" : [
{
"name" : "book.javamvc",
"level":"debug",
"appender-ref": {
"ref":"File"
}
},{
"name" : "some.other.logger",
"level":"debug",
"appender-ref": {
"ref":"File"
}
}
],
"root": {
"level":"debug",
"appender-ref": {
"ref":"File"
}
}
}
}
}
在编码中使用 Log4j
要在 Java MVC 应用中使用 Log4,请确保每个项目都有以下 Gradle 依赖关系:
implementation 'org.apache.logging.log4j:log4j-api:2.11.2'
然后将Logger和LogManager导入到类中,并使用一个静态记录器字段,如下所示:
import org.apache.logging.log4j.*;
public class SomeClass {
private final static Logger LOG =
LogManager.getLogger(SomeClass.class);
...
public void someMethod() {
...
// different logging levels:
LOG.trace("Trace: ...");
LOG.debug("Debug: ...");
LOG.info("Some info: ...");
LOG.warn("Some warning: ...");
LOG.error("Some error: ...");
LOG.fatal("Some fatal error: ...");
...
// Logging in try-catch clauses
try {
...
} catch(Exception e) {
...
LOG.error("Some error", e);
}
}
}
在log4j2.json配置文件中,每个记录器中的level声明一个日志记录阈值:
"loggers": {
"logger": [
{
"name":"book.javamvc",
"level":"debug",
"appender-ref": {
"ref":"appenderName"
}
}
...
]
...
}
级别可以设置为trace、debug、info、warn、error或fatal。
练习
-
练习 1: 将 JSR 47 测井(在
java.util.logging包中)添加到第 4(HelloWorld应用)的App类的@PostConstruct public void init()和@Override public Map<String, Object> getProperties()方法中。讲述如何进入每种方法,以及getProperties()中设置的属性。 -
练习 2: 将服务器范围的 Log4j 日志添加到您的 GlassFish 服务器。选择您的任何项目,并向其中添加 Log4j 日志记录。
摘要
日志记录是任何中级到高级复杂性应用的重要组成部分。当程序通过它的执行路径运行时,几个日志语句描述程序正在做什么,哪些参数被传递给方法调用,局部变量和类字段有什么值以及它们如何改变,作出了哪些决定,等等。这些日志记录信息被收集并发送到文件、数据库、消息队列或其他地方,开发人员和操作团队可以调查程序流,以便修复错误或进行审计。
日志 API 规范 JSR 47 是 Java 的一部分,可以由任何 Java 程序使用,包括 Jakarta EE 服务器应用和 Java MVC 程序。您可以从 https://jcp.org/en/jsr/detail?id=47 下载该规范。
GlassFish 使用这个平台标准 API JSR 47 进行日志记录。除非您更改配置,否则您可以在以下位置找到日志文件:
GLASSFISH_INST/glassfish/domains/domain1/logs/server.log
要使用 JSR 47 方法将诊断输出添加到您自己的类中,请在您的类中编写以下内容:
...
import java.util.logging.Logger;
public class MyClass {
private final static Logger LOG =
Logger.getLogger(MyClass.class.toString());
public void someMethod() {
LOG.entering(this.getClass().toString(),"someMethod");
...
// different logging levels:
LOG.finest("Finest: ...");
LOG.finer("Finer: ...");
LOG.fine("Fine: ...");
LOG.info("Some info: ...");
LOG.warning("Some warning: ...");
LOG.severe("Severe: ...");
...
LOG.exiting(this.getClass().toString(),"someMethod");
}
...
}
对于标准测井,级别依次为严重 ➤ 警告 ➤ 信息 ➤ 精细 ➤ 精细 ➤ 精细。这大大提高了日志的可用性。在项目的早期阶段,您可以将记录阈值设置为一个较低的值,例如fine,您将在记录文件中看到所有fine级和更高级别的记录,直到severe。
JSR 47 标准日志的日志配置依赖于名为logging.properties的配置文件。通常,该文件位于 JDK 安装目录中,但是 GlassFish 服务器会忽略标准日志记录配置,而是使用该文件:
GLASSFISH_INST/glassfish/domains/domain1/ config/logging.properties
Log4j 是一个日志框架,常用于各种 Java 应用。Log4j 可以从 http://logging.apache.org/log4j/2.x/ 下载。
在服务器范围内添加 Log4j 意味着将 Log4j 实现放在一个公共库文件夹中,编写一个 Log4j 配置文件,该文件同时服务于该服务器上运行的所有 Jakarta EE 应用,并让所有应用和应用模块使用 Log4j API。因为这只需要配置一次,然后服务器上所有当前和未来的应用都可以轻松地使用 Log4 进行日志记录,所以这种包含 Log4j 的方式可能是最常见的。相反,您可以在每个应用的基础上添加 Log4j,但是只有当您有重要的理由将 Log4j 封装到应用中时,才应该这样做,例如,如果您还在运行使用旧 Log4j 1.x 版本的遗留应用。
要在服务器范围内添加 Log4j,首先要从 https://logging.apache.org/log4j/2.x/ 下载 Log4j 发行版。然后将log4j-core-2.11.2.jar、log4j-api-2.11.2.jar和log4j-appserver-2.11.2文件(或你下载的任何版本)复制到这个文件夹:
GLASSFISH_INST/glassfish/domains/domain1/
modules/autostart
然后将log4j2.json文件添加到GLASSFISH_INST/glassfish/domains/domain1/lib/classes文件夹中。该文件的基本内容如下:
{
"configuration": {
"name": "Default",
"appenders": {
"RollingFile": {
"name":"File",
"fileName":
"${sys:com.sun.aas.instanceRoot}/logs/log4j.log",
"filePattern":
"${sys:com.sun.aas.instanceRoot}/
logs/log4j-backup-%d{MM-dd-yy-HH-mm-ss}-%i.gz",
"PatternLayout": {
"pattern":
"%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n"
},
"Policies": {
"SizeBasedTriggeringPolicy": {
"size":"10 MB"
}
},
"DefaultRolloverStrategy": {
"max":"10"
}
}
},
"loggers": {
"logger" : [
{
"name" : "book.javamvc",
"level":"debug",
"appender-ref": {
"ref":"File"
}
},{
"name" : "some.other.logger",
"level":"info",
"appender-ref": {
"ref":"File"
}
}
],
"root": {
"level":"error",
"appender-ref": {
"ref":"File"
}
}
}
}
}
如果您认为应该在每个应用的基础上添加 Log4j,并且让服务器上运行的其他应用不受影响,那么您可以将 Log4j 实现添加到您的 web 应用(WAR)中。
要添加 Log4j 实现,您需要更新 Gradle 构建文件中的依赖项。打开build.gradle文件并将其添加到dependencies { }部分:
implementation 'org.apache.logging.log4j:log4j-core
:2.11.2'
implementation 'com.fasterxml.jackson.core:jackson-core
:2.7.4'
implementation 'com.fasterxml.jackson.core:jackson-
databind:2.7.4'
implementation 'com.fasterxml.jackson.core:jackson-
annotations:2.7.4'
这里,中心部分是对log4j-core的依赖;需要依赖于jackson,因为我们将使用 JSON 格式的配置文件,而 Log4j 需要jackson来解析它们。
Log4j 的配置文件需要被称为log4j2.json,它必须放在 web 应用(WARs)的src/main/resources文件夹中。作为一个简单的配置,将log4j2.json的内容设置如下:
{
"configuration": {
"name": "Default",
"appenders": {
"RollingFile": {
"name":"File",
"fileName":
"${sys:com.sun.aas.instanceRoot}/logs/log4j.log",
"filePattern":
"${sys:com.sun.aas.instanceRoot}/
logs/log4j-backup-%d{MM-dd-yy-HH-mm-ss}-%i.gz",
"PatternLayout": {
"pattern":
"%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n"
},
"Policies": {
"SizeBasedTriggeringPolicy": {
"size":"10 MB"
}
},
"DefaultRolloverStrategy": {
"max":"10"
}
}
},
"loggers": {
"logger" : [
{
"name" : "book.javamvc",
"level":"debug",
"appender-ref": {
"ref":"File"
}
},{
"name" : "some.other.logger",
"level":"debug",
"appender-ref": {
"ref":"File"
}
}
],
"root": {
"level":"debug",
"appender-ref": {
"ref":"File"
}
}
}
}
}
要在 Java MVC 应用中使用 Log4,请确保每个项目都有以下 Gradle 依赖关系:
implementation 'org.apache.logging.log4j:log4j-api:2.11.2'
然后在类中导入Logger和LogManager,并使用静态记录器字段,如下所示:
import org.apache.logging.log4j.*;
public class SomeClass {
private final static Logger LOG =
LogManager.getLogger(SomeClass.class);
...
public void someMethod() {
...
// different logging levels:
LOG.trace("Trace: ...");
LOG.debug("Debug: ...");
LOG.info("Some info: ...");
LOG.warn("Some warning: ...");
LOG.error("Some error: ...");
LOG.fatal("Some fatal error: ...");
...
// Logging in try-catch clauses
try {
...
} catch(Exception e) {
...
LOG.error("Some error", e);
}
}
}
在本书的下一章,我们将给出一个全面的 Java MVC 应用的例子。