如何开发一个Java JSP JSTL项目指南 – 小刘技术blog

文章目录[隐藏]

前言

众所周知 JSP 包括 JSTL 是上个世纪的产物,早在2010年就开始逐渐淘汰
2023年正确打开方式是纯前端Vue开发,通过api接口获取后端数据,渲染到页面上
甚至于,现在一提到JSP开发的项目,就让人感觉到落后、LOW、执行效率低等负面印象

那么这篇文章就属于偏偏要反其道而行之

简单解释下为什么用 JSP JSTL

  1. 开发效率高、并不是所有项目都是大型项目需要前后端分离,小型项目更适合后端渲染
  2. 利好SEO,众所周知百度的收录和排名算法还停留在上个世纪,用上个世纪的技术更符合他的胃口
  3. 方便后期维护,发生内容变化后,JSP可以实时热加载,无需重启服务器,也不会被浏览器缓存

一个语言到底好不好用,首先看的是用的人,能发挥出语言几成功力,二是看需求,要避免机关枪打蚊子

折腾

开发环境尽量选新的
(我也是才知道JSTL居然还一直在更新 居然还有3)
JDK 17 Tomcat 10 JSLT 3 MySQL 8

开发工具是 IntelliJ IDEA 2023.2.4 (Ultimate Edition)

首先选择新建项目 New Project
左边栏选择 Jakarta EE,右边如下图

版本选择最新的 Jakarta EE 10 其余默认

开局发现居然是个 Maven 项目
而且已经写好了示例代码

结果一运行 404

简单修改一下 Tomcat 的配置

再次点击运行即可成功访问到demo

由于上次学 JSP 已经是10年前了,开发工具还是 eclipse,没有 maven 导包是手动下载jar包并粘贴到lib目录下,现在一下子过于与时俱进,稍微补了补课

先贴一波文档地址
JSTL 3
https://jakarta.ee/specifications/tags/3.0/tagdocs/index.html
https://jakarta.ee/specifications/tags/3.0/jakarta-tags-spec-3.0.html

稍微写个简单的demo

先删了一些用不到的文件和文件夹
保留下的目录如下

jsp2023/
|-- src/
|   |-- main/
|   |   |-- java/
|   |   |   |-- com.zzzmh.jsp2023/
|   |   |   |   |-- HelloServlet.java
|   |   |-- resources/
|   |   |-- webapp/
|   |   |   |-- WEB-INF/
|   |   |   |   |-- web.xml
|   |   |   |-- index.jsp
`-- pom.xml

直连MySQL
maven加JSTL JDBC依赖
pom.xml

<dependency>
    <groupId>org.glassfish.web</groupId>
    <artifactId>jakarta.servlet.jsp.jstl</artifactId>
    <version>3.0.0</version>
</dependency>
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <version>8.2.0</version>
</dependency>

resources目录下加MySQL配置文件config.properties

  1. driver=com.mysql.cj.jdbc.Driver
  2. url=jdbc:mysql://127.0.0.1:3306/test?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&tinyInt1isBit=false&allowPublicKeyRetrieval=true
  3. user=root
  4. password=pwd

index.jsp加代码

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="sql" uri="jakarta.tags.sql" %>
<%@ taglib prefix="fmt" uri="jakarta.tags.fmt" %>
<%@ taglib prefix="fn" uri="jakarta.tags.functions" %>
<%-- 从config.properties文件读取MySQL配置 --%>
<fmt:bundle basename="config">
    <fmt:message key="url" var="url"/>
    <fmt:message key="driver" var="driver"/>
    <fmt:message key="user" var="user"/>
    <fmt:message key="password" var="password"/>
</fmt:bundle>
<%-- JDBC 直连数据库 --%>
<sql:setDataSource var="dataSource" url="${url}" driver="${driver}" user="${user}" password="${password}"/>
<%-- 查询User表所有字段 --%>
<sql:query var="users" dataSource="${dataSource}">
    SELECT * FROM users WHERE del_flag = 0 ORDER BY create_time ASC
</sql:query>
<%-- html response 顶部不留白 --%>
<%@ page trimDirectiveWhitespaces="true" %>
<!DOCTYPE html>
<html>
<head>
    <title>JSP - Hello World</title>
</head>
<body>
<h1>${"Hello World!"}</h1>
<%-- 读取到的数据库配置文件参数 --%>
<div>
    <p>${url}</p>
    <p>${driver}</p>
    <p>${user}</p>
    <p>${password}</p>
</div>
<%-- 便利数据库查询到的users --%>
<table>
    <c:forEach var="user" items="${users.rows}">
        <tr>
            <td>${user.id}"</td>
            <td>${user.username}"</td>
            <td>${user.password}"</td>
            <td>${user.status}</td>
            <td>${user.create_time}</td>
            <td>${user.update_time}</td>
            <td>${user.del_flag}"</td>
        </tr>
    </c:forEach>
</table>
</body>
</html>

启动Tomcat可以看到执行结果 已成功直连数据库

这里有个小问题,时间格式不对,我试了用JSTL自带的时间格式化工具,会报500错误,LocalDateTime不能转换成Date
代码如下

<%-- 便利数据库查询到的users --%>
<table>
    <c:forEach var="user" items="${users.rows}">
        <tr>
            <td>${user.id}"</td>
            <td>${user.username}"</td>
            <td>${user.password}"</td>
            <td>${user.status}</td>
            <td><fmt:formatDate value="${user.create_time}" pattern="yyyy-MM-dd HH:mm:ss"/></td>
            <td><fmt:formatDate value="${user.update_time}" pattern="yyyy-MM-dd HH:mm:ss"/></td>
            <td>${user.del_flag}"</td>
        </tr>
    </c:forEach>
</table>

报错

jakarta.el.ELException: 无法将类型为[class java.time.LocalDateTime]的[2023-11-15T17:13:13]转换为[class java.util.Date]
    org.apache.el.lang.ELSupport.coerceToType(ELSupport.java:601)
    org.apache.el.ExpressionFactoryImpl.coerceToType(ExpressionFactoryImpl.java:46)
    jakarta.el.ELContext.convertToType(ELContext.java:319)
    org.apache.el.ValueExpressionImpl.getValue(ValueExpressionImpl.java:192)
    org.apache.jasper.runtime.PageContextImpl.proprietaryEvaluate(PageContextImpl.java:701)
    org.apache.jsp.index_jsp._jspx_meth_fmt_005fformatDate_005f0(index_jsp.java:513)
    org.apache.jsp.index_jsp._jspx_meth_c_005fforEach_005f0(index_jsp.java:468)
    org.apache.jsp.index_jsp._jspService(index_jsp.java:182)
    org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:70)
    jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)
    org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:456)
    org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:380)
    org.apache.jasper.servlet.JspServlet.service(JspServlet.java:328)
    jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)
    org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)

应该是MySQL或者JSTL最新版,默认用LocalDateTime记录时间导致的,JSTL自带方法没有能格式化LocalDateTime的方法,只有2个思路,简单点就是转成时间戳,然后用js格式化,想后端转就要用到JSTL的funcitons,写自定义工具类实现,这个以后在很多地方都能用到,包括不想在JSP里写SQL,也可以写自定义方法获取数据,数据可以写在Java代码中,就可以用框架实现,比如MyBatisPlus,也可以实现简单的Redis功能。

JSTL自定义方法
新建一个工具类
DateUtils.java

/**
 * 时间工具类
 * @author zzzmh
 * @email admin@zzzmh.cn
 * @date 2023-11-15 17:37:00
 */
public class DateUtils {
    /**
     * 格式化LocalDateTime到String
     */
    public static String formatLocalDateTime(LocalDateTime localDateTime){
        return localDateTime == null ? "" : localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    }
}

然后参考JSP之前引入的
<%@ taglib prefix="fn" uri="jakarta.tags.functions" %>
通过 Ctrl + 左键 可以点进去学习别人的functions是怎么写的
全文太长了我只截取一小段 大概长这样

<?xml version="1.0" encoding="UTF-8" ?>
<taglib 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-jsptaglibrary_3_0.xsd"
    version="3.0">
  <description>Tags 3.0 functions library</description>
  <display-name>Tags functions</display-name>
  <tlib-version>3.0</tlib-version>
  <short-name>fn</short-name>
  <uri>jakarta.tags.functions</uri>
  <function>
    <description>
      Tests if an input string contains the specified substring.
    </description>
    <name>contains</name>
    <function-class>org.apache.taglibs.standard.functions.Functions</function-class>
    <function-signature>boolean contains(java.lang.String, java.lang.String)</function-signature>
    <example>
      <c:if test="${fn:contains(name, searchString)}">
    </example>
  </function>
  <function>
    <description>
      Tests if an input string contains the specified substring in a case insensitive way.
    </description>
    <name>containsIgnoreCase</name>
    <function-class>org.apache.taglibs.standard.functions.Functions</function-class>
    <function-signature>boolean containsIgnoreCase(java.lang.String, java.lang.String)</function-signature>
    <example>
      <c:if test="${fn:containsIgnoreCase(name, searchString)}">
    </example>
  </function>
</taglib>

然后就可以学着他的格式自己写一个自定义工具类
比如说就叫utils
在目录 WEB-INF 下
新建文件 utils.tld

<?xml version="1.0" encoding="UTF-8" ?>
<taglib 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-jsptaglibrary_3_0.xsd"
        version="3.0">
    <description>Tags 3.0 Custom Utils</description>
    <display-name>Utils</display-name>
    <tlib-version>3.0</tlib-version>
    <short-name>utils</short-name>
    <uri>jakarta.tags.utils</uri>
    <function>
        <!-- 描述和例子是可有可无的 关键是中间3个 -->
        <description>
            LocalDateTime Format To String yyyy-MM-dd HH:mm:ss
        </description>
        <!-- jstl中调用此方法的方法名 -->
        <name>formatLocalDateTime</name>
        <!-- 此方法所在类的具体位置 -->
        <function-class>com.zzzmh.jsp2023.utils.DateUtils</function-class>
        <!-- 传入返回参数类型加方法名(DateUtils类中的方法名) -->
        <function-signature>java.lang.String formatLocalDateTime(java.time.LocalDateTime)</function-signature>
        <example>
            ${utils:formatLocalDateTime(obj.create_time)}
        </example>
    </function>
</taglib>

具体用法是在JSP的EL表达式中
${utils:formatLocalDateTime(user.create_time)}

完整代码 index.jsp

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="sql" uri="jakarta.tags.sql" %>
<%@ taglib prefix="fmt" uri="jakarta.tags.fmt" %>
<%@ taglib prefix="fn" uri="jakarta.tags.functions" %>
<%@ taglib prefix="utils" uri="jakarta.tags.utils" %>
<%-- 从config.properties文件读取MySQL配置 --%>
<fmt:bundle basename="config">
    <fmt:message key="url" var="url"/>
    <fmt:message key="driver" var="driver"/>
    <fmt:message key="user" var="user"/>
    <fmt:message key="password" var="password"/>
</fmt:bundle>
<%-- JDBC 直连数据库 --%>
<sql:setDataSource var="dataSource" url="${url}" driver="${driver}" user="${user}" password="${password}"/>
<%-- 查询User表所有字段 --%>
<sql:query var="users" dataSource="${dataSource}">
    SELECT * FROM users WHERE del_flag = 0 ORDER BY create_time ASC
</sql:query>
<%-- html response 顶部不留白 --%>
<%@ page trimDirectiveWhitespaces="true" %>
<!DOCTYPE html>
<html>
<head>
    <title>JSP - Hello World</title>
</head>
<body>
<h1>${"Hello World!"}</h1>
<%-- 读取到的数据库配置文件参数 --%>
<div>
    <p>${url}</p>
    <p>${driver}</p>
    <p>${user}</p>
    <p>${password}</p>
</div>
<%-- 便利数据库查询到的users --%>
<table>
    <c:forEach var="user" items="${users.rows}">
        <tr>
            <td>${user.id}</td>
            <td>${user.username}</td>
            <td>${user.password}</td>
            <td>${user.status}</td>
            <td>${utils:formatLocalDateTime(user.create_time)}</td>
            <td>${utils:formatLocalDateTime(user.update_time)}</td>
            <td>${user.del_flag}</td>
        </tr>
    </c:forEach>
</table>
</body>
</html>

重启Tomcat发现时间已经显示正常了

同理自己实现一个Redis工具类或者Mybatis实现一个service也都不是难事了

最后总结一下前后端渲染最大的区别
右击浏览器网页,选择查看网页源代码
可以看到效果是这样的

<!DOCTYPE html>
<html>
<head>
    <title>JSP - Hello World</title>
</head>
<body>
<h1>Hello World!</h1>
<div>
    <p>jdbc:mysql://127.0.0.1:3306/test?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&tinyInt1isBit=false&allowPublicKeyRetrieval=true</p>
    <p>com.mysql.cj.jdbc.Driver</p>
    <p>root</p>
    <p>pwd</p>
</div>
<table>
    <tr>
            <td>1</td>
            <td>张三</td>
            <td>e10adc3949ba59abbe56e057f20f883e</td>
            <td>0</td>
            <td>2023-11-15 17:13:13</td>
            <td>2023-11-15 17:13:13</td>
            <td>0</td>
        </tr>
    <tr>
            <td>2</td>
            <td>张四</td>
            <td>e10adc3949ba59abbe56e057f20f883e</td>
            <td>0</td>
            <td>2023-11-15 17:13:22</td>
            <td>2023-11-15 17:13:22</td>
            <td>0</td>
        </tr>
    <tr>
            <td>3</td>
            <td>张五</td>
            <td>e10adc3949ba59abbe56e057f20f883e</td>
            <td>0</td>
            <td>2023-11-15 17:13:31</td>
            <td>2023-11-15 17:13:31</td>
            <td>0</td>
        </tr>
    </table>
</body>
</html>

这就是意味着,搜索引擎爬虫收录起来更方便无脑了
部分客户端性能羸弱下,打开速度会比前端渲染更快,且第一时间能看到部分内容,不至于白屏。
我观察了B站、简书、知乎
都是后端渲染一部分固定的数据,比如文章标题、正文
前端渲染其余动态的数据,比如评论区,相关文章推荐
这样也能方便搜索引擎收录,否则你要是纯前端渲染,爬虫爬到的效果就是几行html,引入几个js,没了,现在聪明的搜索引擎已经可以实现模拟Chrome环境渲染并爬取结果了,笨蛋搜索引擎还原地踏步。

补充
另外他也不是只能同步加载,他也是可以异步的
Servlet就刚好适合写API接口,返回JSON格式数据。
简单举个例子

用到2个新的依赖

maven下新增FastJSON

<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.42</version>
</dependency>

InputSteam 转 String 用的是 Springboot里扒下来工具类
StreamUtils.java

package com.zzzmh.jsp2023.utils;
import java.io.*;
import java.nio.charset.Charset;
/**
 * 这里照抄一下Springboot
 * org.springframework:spring-core:6.0.13
 * utils目录下StreamUtils方法
 * 实现Request.InputSteam转String
 * 这里图省事把原本的非空判断去掉了 如果放到正式环境需要先判断InputStream非空
 */
public abstract class StreamUtils {
    public static final int BUFFER_SIZE = 8192;
    private static final byte[] EMPTY_CONTENT = new byte[0];
    public StreamUtils() {
    }
    public static byte[] copyToByteArray(InputStream in) throws IOException {
        return in == null ? EMPTY_CONTENT : in.readAllBytes();
    }
    public static String copyToString(InputStream in, Charset charset) throws IOException {
        if (in == null) {
            return "";
        } else {
            StringBuilder out = new StringBuilder();
            InputStreamReader reader = new InputStreamReader(in, charset);
            char[] buffer = new char[8192];
            int charsRead;
            while((charsRead = reader.read(buffer)) != -1) {
                out.append(buffer, 0, charsRead);
            }
            return out.toString();
        }
    }
}

核心方法
首先在 com.zzzmh.jsp2023 下新建目录 servlet
再在 servlet 下新建文件 GetCommentServlet.java

package com.zzzmh.jsp2023.servlet;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.zzzmh.jsp2023.utils.StreamUtils;
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.io.PrintWriter;
import java.nio.charset.StandardCharsets;
/**
 * 获取评论区数据接口
 */
@WebServlet(name = "GetCommentServlet", value = "/getComments")
public class GetCommentServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 接收传入参数
        JSONObject params = JSONObject.parseObject(StreamUtils.copyToString(req.getInputStream(), StandardCharsets.UTF_8));
        System.out.println("收到参数 id:" + params.getIntValue("id"));
        // 这里就手动模拟一个返回值,demo写全套增删改查没必要了,相信大家写增删改查也已经写吐了
        JSONObject result = JSONObject.of(
                "code", 0,
                "message", "success",
                "data", JSONArray.of(
                        JSONObject.of("id", 1, "name", "张三", "message", "挽尊"),
                        JSONObject.of("id", 2, "name", "张四", "message", "路过,打卡~"),
                        JSONObject.of("id", 3, "name", "张五", "message", "我是谁?我在哪?今夕是何年?")
                )
        );
        resp.setContentType("application/json");
        PrintWriter writer = resp.getWriter();
        writer.print(result.toJSONString());
        writer.flush();
        writer.close();
    }
}

目前只能用工具模拟post请求,我这里简单用postman,一次跑通

IDEA Console也能收到传入参数

顺手补上JSP里用JavaScript请求的简单实现


  1. <script>
  2. ajax("post", "getComments", JSON.stringify({"id": 1}), function (result) {
  3. console.log(result);
  4. }, function (error) {
  5. console.error(error);
  6. });
  7. /**
  8. * 手搓个简单的ajax
  9. */
  10. function ajax(method, url, data, success, error) {
  11. const xhr = new XMLHttpRequest();
  12. xhr.open(method, url, true);
  13. xhr.setRequestHeader("content-type", "application/json");
  14. xhr.timeout = 60000;
  15. xhr.send(data);
  16. xhr.onreadystatechange = function () {
  17. // 仅处理完成状态
  18. if (xhr.readyState === 4) {
  19. // 状态200判断为成功
  20. if (xhr.status === 200) {
  21. if (success) {
  22. success(JSON.parse(xhr.responseText));
  23. }
  24. } else {
  25. if (error) {
  26. error();
  27. }
  28. }
  29. }
  30. }
  31. }
  32. </script>

控制台效果

就先写到这 累了

在线客服
在线客服
我们将24小时内回复。
2025-12-05 13:26:50
您好,有任何疑问请与我们联系!
您的工单我们已经收到,我们将会尽快跟您联系!
取消
选择聊天工具:
Verified by MonsterInsights