Jakarta EE 在 IntelliJ IDEA 中开发简单留言板应用的实验指导(附完整代码)
摘要:实验基于Jakarta EE 9+(兼容Tomcat 10+)、Maven作为构建工具,并在IntelliJ IDEA 2023.2(Community版免费)中进行。项目使用Maven Archetype WebApp模板生成基础结构,然后升级到Jakarta EE。
这个实验实现了一个简单的Mini Message Board:
访问
显示留言表单和已提交的留言列表(按时间倒序)。表单包含昵称(nickname)和留言内容(content)两个字段,均为必填。长度限制:nickname ≤ 50字符,content ≤ 500字符(服务端验证,无效时显示错误消息)。提交后,Servlet处理POST请求,将有效留言记录到内存(使用CopyOnWriteArrayList);重定向刷新页面。
/board
我会提供:
详细实验步骤:从项目创建到测试的指导。代码:完整、可复制的pom.xml、Java类(Servlet)、JSP文件、web.xml等。代码解释:每个代码块中添加详细注释,解释实现逻辑、Jakarta EE/HTTP相关知识和关键点。
实验不使用数据库(按要求用内存);扩展部分会添加持久化。
实验准备
前提:IntelliJ IDEA 2023.2已安装,JDK 17+配置好(File > Project Structure > SDKs > Add JDK)。Tomcat 10+下载(https://tomcat.apache.org/),解压到本地目录(e.g., C:Tomcat10)。时间:约1小时(包括调试)。测试工具:浏览器(Chrome)、curl命令(Windows CMD)。
详细实验步骤
步骤1: 使用Maven Archetype创建项目
打开IntelliJ IDEA 2023.2,点击“File > New > Project”。选择“Maven Archetype”。配置:
Name:MiniMessageBoard。Location:选择保存目录。Archetype:点击“Add Archetype…”,填写:
GroupId:org.apache.maven.archetypesArtifactId:maven-archetype-webappVersion:1.4(稳定版)。 点击“OK”,然后“Next”。 项目属性:
GroupId:com.exampleArtifactId:minimessageboardVersion:1.0-SNAPSHOT 点击“Create”。IntelliJ生成项目,包括pom.xml和src/main/webapp/index.jsp。
步骤2: 更新pom.xml到Jakarta EE并添加依赖
双击pom.xml,替换为以下内容(升级到Jakarta EE 9,添加JSTL for JSP):
解释:Archetype默认旧Java EE,更新依赖确保jakarta.*命名空间;JSTL用于JSP标签(如<c:forEach>循环显示列表)。
<?xml version="1.0" encoding="UTF-8"?>
<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.example</groupId>
<artifactId>minimessageboard</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<!-- Jakarta EE Web API (包含Servlet, JSP等) -->
<dependency>
<groupId>jakarta.platform</groupId>
<artifactId>jakarta.jakartaee-web-api</artifactId>
<version>9.1.0</version>
<scope>provided</scope>
</dependency>
<!-- JSTL for JSP (标签库, 用于<c:forEach>等) -->
<dependency>
<groupId>jakarta.servlet.jsp.jstl</groupId>
<artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>jakarta.servlet.jsp.jstl</artifactId>
<version>2.0.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.2</version>
</plugin>
</plugins>
</build>
</project>
右键pom.xml > Maven > Reload project(下载依赖)。
步骤3: 更新web.xml(兼容Jakarta EE)
在
中替换为以下内容:
src/main/webapp/WEB-INF/web.xml
解释:Archetype生成的web.xml是旧版本(javax),更新到jakarta命名空间,确保兼容Jakarta EE 9+。无需额外配置,因为Servlet用注解注册。
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
version="5.0">
<!-- 无需额外配置, Servlet使用注解@WebServlet -->
</web-app>
步骤4: 创建留言实体类(Message.java)
在
下右键New > Java Class创建Message.java。
src/main/java/com/
解释:这是一个简单POJO(Plain Old Java Object),用于存储留言数据(昵称、内容、时间)。使用java.time.LocalDateTime记录时间,便于倒序排序。非public类避免文件命名问题。
package com.example;
import java.time.LocalDateTime;
// 留言实体类 (POJO), 用于内存存储
class Message {
private String nickname; // 昵称
private String content; // 留言内容
private LocalDateTime timestamp; // 时间戳, 用于倒序排序
// 构造函数
public Message(String nickname, String content) {
this.nickname = nickname;
this.content = content;
this.timestamp = LocalDateTime.now(); // 当前时间
}
// Getters (用于JSP访问)
public String getNickname() {
return nickname;
}
public String getContent() {
return content;
}
public LocalDateTime getTimestamp() {
return timestamp;
}
}
代码解释:
LocalDateTime:从Java 8引入,用于记录精确时间(包括日期和时分秒),便于后续列表排序(倒序显示最新留言)。非public:避免“公共类需匹配文件名”错误;这是一个内部类,供Servlet使用。没有Setter:留言一旦创建不可修改,符合简单设计。
步骤5: 创建Servlet类(BoardServlet.java)
在
下New > Java Class创建BoardServlet.java。
src/main/java/com/
解释:这是核心类,使用@WebServlet注解注册路径/board。doGet处理GET请求(显示表单+列表),doPost处理POST请求(验证+存储+重定向)。内存存储使用CopyOnWriteArrayList(线程安全,适合并发读写)。防XSS通过简单字符串替换实现。响应设置UTF-8避免中文乱码。
package com.example;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
// 使用@WebServlet注解注册Servlet, 路径为/board
@WebServlet(name = "BoardServlet", value = "/board")
public class BoardServlet extends HttpServlet {
// 内存存储: CopyOnWriteArrayList (线程安全, 适合高读低写)
private final List<Message> messages = new CopyOnWriteArrayList<>();
// doGet: 处理GET请求, 显示表单和留言列表 (按时间倒序)
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 设置响应内容类型为HTML, UTF-8编码 (防止中文乱码)
resp.setContentType("text/html; charset=UTF-8");
// 获取错误消息 (从session, 用于POST验证失败)
String error = (String) req.getSession().getAttribute("error");
req.getSession().removeAttribute("error"); // 清空, 避免重复显示
// 排序留言列表 (按时间倒序, 使用Collections.sort)
List<Message> sortedMessages = new CopyOnWriteArrayList<>(messages); // 复制以排序
sortedMessages.sort((m1, m2) -> m2.getTimestamp().compareTo(m1.getTimestamp())); // 倒序 (最新先)
// 将排序列表传递到JSP
req.setAttribute("messages", sortedMessages);
req.setAttribute("error", error);
// 转发到JSP页面渲染
req.getRequestDispatcher("/board.jsp").forward(req, resp);
}
// doPost: 处理POST请求, 验证表单, 存储留言, 重定向到GET
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取参数 (UTF-8编码已由Tomcat处理, 但确认)
String nickname = req.getParameter("nickname");
String content = req.getParameter("content");
// 验证: 必填和长度限制
String error = null;
if (nickname == null || nickname.trim().isEmpty()) {
error = "昵称不能为空";
} else if (nickname.length() > 50) {
error = "昵称长度不能超过50字符";
} else if (content == null || content.trim().isEmpty()) {
error = "留言内容不能为空";
} else if (content.length() > 500) {
error = "留言内容长度不能超过500字符";
}
if (error != null) {
// 验证失败: 保存错误到session, 重定向回表单
req.getSession().setAttribute("error", error);
resp.sendRedirect(req.getContextPath() + "/board");
return;
}
// XSS转义: 简单替换危险字符 (防止<script>注入)
nickname = escapeHtml(nickname);
content = escapeHtml(content);
// 创建留言对象并添加到列表
Message message = new Message(nickname, content);
messages.add(message);
// 重定向到GET方法刷新页面
resp.sendRedirect(req.getContextPath() + "/board");
}
// 简单XSS转义方法 (替换< > & " ')
private String escapeHtml(String input) {
if (input == null) return "";
return input.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace(""", """)
.replace("'", "'");
}
}
代码解释:
@WebServlet:注解注册Servlet到路径/board,无需web.xml手动配置。这是Jakarta EE的现代方式,简化部署。CopyOnWriteArrayList:线程安全的List,适合多线程环境(Web应用可能并发),添加时复制数组,确保读操作(列表显示)高效无锁。doGet:处理HTTP GET请求,生成响应。设置Content-Type为text/html; charset=UTF-8确保中文显示正常(HTTP响应头语义:告诉浏览器内容类型和编码)。使用request.setAttribute传递数据到JSP(请求作用域)。doPost:处理HTTP POST请求(表单提交)。通过request.getParameter获取参数(请求体解析)。验证后,转义XSS(替换HTML特殊字符,防止注入攻击,如用户输入<script>alert(1)</script>会被转义为安全字符串)。存储到内存列表,重定向(resp.sendRedirect,使用HTTP 302状态码,响应头Location指定新URL)。排序:使用sort和Comparator,按timestamp倒序(最新留言先显示)。错误处理:用session存储临时错误消息(请求间传递),在doGet显示。HTTP知识:请求行(GET/POST /board HTTP/1.1)、头(Content-Type)、体(表单数据);响应状态码(302 for重定向)、头(Location)、体(HTML)。
步骤6: 创建JSP页面(board.jsp)
在
下New > JSP创建board.jsp。
src/main/webapp
解释:JSP用于动态HTML生成,混合Java代码和标签。使用JSTL的<c:if>和<c:forEach>处理条件(错误显示)和循环(留言列表)。页面编码UTF-8确保中文正常。
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<title>Mini Message Board</title>
</head>
<body>
<h1>Mini Message Board</h1>
<!-- 显示错误消息 (如果存在) -->
<c:if test="${not empty error}">
<p>${error}</p>
</c:if>
<!-- 留言表单 -->
<form action="${pageContext.request.contextPath}/board" method="post">
昵称 (最多50字符): <input type="text" name="nickname" required maxlength="50"><br>
留言内容 (最多500字符): <textarea name="content" required maxlength="500"></textarea><br>
<input type="submit" value="提交">
</form>
<!-- 留言列表 (按时间倒序) -->
<h2>留言列表</h2>
<ul>
<c:forEach items="${messages}" var="message">
<li>
<strong>${message.nickname}</strong> (${message.timestamp}): ${message.content}
</li>
</c:forEach>
</ul>
</body>
</html>
代码解释:
<%@ page %>:指定页面编码UTF-8(响应体语义:确保浏览器正确解析中文)。<%@ taglib %>:引入JSTL核心标签库,用于<c:if>(条件显示错误)和<c:forEach>(循环遍历messages列表,动态生成HTML)。表单:action指向/board (POST到Servlet),method=”post”(HTTP请求行:POST /board HTTP/1.1)。使用HTML required/maxlength for客户端验证(服务端在Servlet再验证)。列表:从request attribute “messages”获取数据,按倒序显示(Servlet已排序)。每个<li>对应一条留言,展示昵称、时间、内容。HTTP相关:表单提交生成请求体(application/x-www-form-urlencoded格式,参数nickname=xx&content=yy);Servlet解析后生成响应体(HTML)。
步骤7: 配置Tomcat并运行项目
Run > Edit Configurations > + > Tomcat Server > Local。Server tab:选择Tomcat 10+目录。Deployment tab:+ > Artifact > minimessageboard:war exploded。Application context:/(根路径)。保存,运行(绿色按钮)。
步骤8: 运行与测试
访问http://localhost:8080/board:显示表单和空列表。提交留言(POST):
输入昵称”User1″(≤50),内容”Hello”(≤500) → 重定向,列表显示(最新在上)。无效输入(空或超长) → 显示错误消息,不存储。 测试中文:输入中文昵称/内容,确认无乱码(UTF-8生效)。curl测试:
GET列表:
(返回HTML)。POST提交:
curl http://localhost:8080/board
(模拟表单,验证重定向)。 验证XSS:输入content=”<script>alert(1)</script>” → 显示为转义字符串,不执行脚本。用浏览器开发者工具(F12 > Network)查看请求/响应:请求行(POST /board HTTP/1.1)、头(Content-Type)、体(参数);响应状态码(302)、头(Location)、体(HTML)。
curl -d "nickname=User2&content=Test" http://localhost:8080/board
实验观察:分析curl -v输出,理解HTTP消息结构(请求头如User-Agent,响应头如Content-Type)。
4. 思考与扩展:把留言“持久化”保存
目标分析
问题:当前内存存储(CopyOnWriteArrayList)重启Tomcat后丢失留言。持久化确保重启后数据存在,理解状态管理(内存 vs. 持久)、资源管理(连接池/文件I/O)。对比内存版:内存快但易失;持久化慢但可靠,需处理连接打开/关闭、事务、错误恢复。方案A (文件持久化, JSON):简单,用文件存储(易实现,无数据库)。方案B (嵌入式数据库, H2):推荐,结构化+自动表管理,适合复杂查询。
方案A: 文件持久化(JSON)
思路:使用Gson库序列化List<Message>到JSON文件(e.g., messages.json)。init()加载文件,doPost后保存文件。重启后自动加载。优缺点:简单、无依赖;但并发需锁,文件大时慢。代码扩展(在BoardServlet.java添加):
// 添加import
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.ArrayList;
// 修改messages为ArrayList (非CopyOnWrite, 因为有锁)
private final List<Message> messages = new ArrayList<>();
private final String storageFile = "messages.json"; // 文件路径 (项目根或指定)
// init: 从JSON加载
@Override
public void init() throws ServletException {
super.init();
try (FileReader reader = new FileReader(storageFile)) {
Gson gson = new Gson();
Type listType = new TypeToken<ArrayList<Message>>(){}.getType();
List<Message> loaded = gson.fromJson(reader, listType);
if (loaded != null) messages.addAll(loaded);
} catch (IOException e) {
// 文件不存在, 初始化空
}
}
// doPost后保存到JSON
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// ... 原验证和添加message
messages.add(message);
// 保存到文件 (同步块防并发)
synchronized (messages) {
try (FileWriter writer = new FileWriter(storageFile)) {
new Gson().toJson(messages, writer);
} catch (IOException e) {
e.printStackTrace(); // 生产用日志
}
}
resp.sendRedirect(req.getContextPath() + "/board");
}
// destroy: 保存 (可选, 但init加载已够)
@Override
public void destroy() {
// ... 保存类似doPost
}
pom.xml添加Gson:
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.9</version>
</dependency>
测试:提交留言,重启Tomcat,访问/board确认数据存在。解释:JSON序列化简单(Gson处理),文件I/O管理资源(try-with-resources自动关闭)。对比内存:添加持久化开销,但重启不丢数据。
方案B: 嵌入式数据库持久化(H2,推荐)
思路:用JPA/Hibernate存储Message实体到H2(文件模式)。init()无需加载(数据库持久),doPost persist。重启后数据自动可用。
优缺点:自动事务/查询;需依赖,但集成简单。
代码扩展:
添加persistence.xml(src/main/resources/META-INF/):
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="3.0" xmlns="https://jakarta.ee/xml/ns/persistence">
<persistence-unit name="MessagePU" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<class>com.example.Message</class>
<properties>
<property name="jakarta.persistence.jdbc.driver" value="org.h2.Driver"/>
<property name="jakarta.persistence.jdbc.url" value="jdbc:h2:file:./messagedb"/>
<property name="jakarta.persistence.jdbc.user" value="sa"/>
<property name="jakarta.persistence.jdbc.password" value=""/>
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
<property name="hibernate.hbm2ddl.auto" value="update"/>
<property name="hibernate.show_sql" value="true"/>
</properties>
</persistence-unit>
</persistence>
修改Message.java(添加JPA注解):
package com.example;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import java.time.LocalDateTime;
@Entity // JPA实体
public class Message {
@Id @GeneratedValue // 自增ID
private Long id;
// ... 原字段和方法
}
修改BoardServlet.java(使用JPA):
// 添加import
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;
import jakarta.persistence.Query;
import java.util.List;
public class BoardServlet extends HttpServlet {
private EntityManagerFactory emf = Persistence.createEntityManagerFactory("MessagePU");
// doGet: 从数据库查询列表
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
EntityManager em = emf.createEntityManager();
Query query = em.createQuery("SELECT m FROM Message m ORDER BY m.timestamp DESC");
List<Message> sortedMessages = query.getResultList();
em.close();
req.setAttribute("messages", sortedMessages);
// ... 原错误处理和转发
}
// doPost: 存储到数据库
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// ... 原验证
Message message = new Message(nickname, content);
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
em.persist(message); // 持久化
em.getTransaction().commit();
em.close();
resp.sendRedirect(req.getContextPath() + "/board");
}
@Override
public void destroy() {
emf.close();
super.destroy();
}
}
pom.xml添加Hibernate/H2(同前述扩展)。
测试:提交留言,重启Tomcat,访问/board确认数据存在(从H2加载)。
解释:JPA简化持久化(em.persist自动insert);文件模式确保重启不丢。对比内存:添加数据库连接管理(emf/em),资源关闭防泄漏(destroy中)。