Jakarta EE 在 IntelliJ IDEA 中开发简单留言板应用的实验指导(附完整代码)

内容分享2小时前发布
0 0 0

Jakarta EE 在 IntelliJ IDEA 中开发简单留言板应用的实验指导(附完整代码)

       摘要:实验基于Jakarta EE 9+(兼容Tomcat 10+)、Maven作为构建工具,并在IntelliJ IDEA 2023.2(Community版免费)中进行。项目使用Maven Archetype WebApp模板生成基础结构,然后升级到Jakarta EE。

这个实验实现了一个简单的Mini Message Board:

访问
/board
显示留言表单和已提交的留言列表(按时间倒序)。表单包含昵称(nickname)留言内容(content)两个字段,均为必填。长度限制:nickname ≤ 50字符,content ≤ 500字符(服务端验证,无效时显示错误消息)。提交后,Servlet处理POST请求,将有效留言记录到内存(使用CopyOnWriteArrayList);重定向刷新页面。

我会提供:

详细实验步骤:从项目创建到测试的指导。代码:完整、可复制的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)


src/main/java/com/
下右键New > Java Class创建Message.java。
解释:这是一个简单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)


src/main/java/com/
下New > Java Class创建BoardServlet.java。
解释:这是核心类,使用@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("&", "&amp;")
                    .replace("<", "&lt;")
                    .replace(">", "&gt;")
                    .replace(""", "&quot;")
                    .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)


src/main/webapp
下New > JSP创建board.jsp。
解释: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列表:
curl http://localhost:8080/board
(返回HTML)。POST提交:
curl -d "nickname=User2&content=Test" http://localhost:8080/board
(模拟表单,验证重定向)。 验证XSS:输入content=”<script>alert(1)</script>” → 显示为转义字符串,不执行脚本。用浏览器开发者工具(F12 > Network)查看请求/响应:请求行(POST /board HTTP/1.1)、头(Content-Type)、体(参数);响应状态码(302)、头(Location)、体(HTML)。
实验观察:分析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中)。

© 版权声明

相关文章

暂无评论

none
暂无评论...