关于2023年如何开发一个Java JSP JSTL项目指南
                                    
                                        2023-11-16
                                        阅读 {{counts.readCount}}
                                        评论 {{counts.commentCount}}
                                    
                                    
                                    ## 前言
众所周知 `JSP` 包括 `JSTL` 是上个世纪的产物,早在2010年就开始逐渐淘汰
2023年正确打开方式是纯前端`Vue`开发,通过`api`接口获取后端数据,渲染到页面上
甚至于,现在一提到JSP开发的项目,就让人感觉到落后、LOW、执行效率低等负面印象
那么这篇文章就属于偏偏要反其道而行之
写一篇如何在2023年用JSP开发网页的入门指南说明书
简单解释下为什么用 `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

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

<br>
先贴一波文档地址
**JSTL 3**
[https://jakarta.ee/specifications/tags/3.0/tagdocs/index.html](https://jakarta.ee/specifications/tags/3.0/tagdocs/index.html)
[https://jakarta.ee/specifications/tags/3.0/jakarta-tags-spec-3.0.html](https://jakarta.ee/specifications/tags/3.0/jakarta-tags-spec-3.0.html)
<br>
稍微写个简单的demo
先删了一些用不到的文件和文件夹
保留下的目录如下
```treeview
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`
```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`
```properties
driver=com.mysql.cj.jdbc.Driver
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
user=root
password=pwd
```
`index.jsp`加代码
```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
代码如下
```jsp
<%-- 便利数据库查询到的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>
```
报错
```java
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功能。
<br>
**JSTL自定义方法**
新建一个工具类
DateUtils.java
```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
<?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
<?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`
```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也都不是难事了
最后总结一下前后端渲染最大的区别
`右击`浏览器网页,选择`查看网页源代码`
可以看到效果是这样的
```html
<!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
```xml
<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.42</version>
</dependency>
```
`InputSteam` 转 `String` 用的是 Springboot里扒下来工具类
`StreamUtils.java`
```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`
```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请求的简单实现
```javascript
<script>
ajax("post", "getComments", JSON.stringify({"id": 1}), function (result) {
    console.log(result);
}, function (error) {
    console.error(error);
});
/**
 * 手搓个简单的ajax
 */
function ajax(method, url, data, success, error) {
    const xhr = new XMLHttpRequest();
    xhr.open(method, url, true);
    xhr.setRequestHeader("content-type", "application/json");
    xhr.timeout = 60000;
    xhr.send(data);
    xhr.onreadystatechange = function () {
        // 仅处理完成状态
        if (xhr.readyState === 4) {
            // 状态200判断为成功
            if (xhr.status === 200) {
                if (success) {
                    success(JSON.parse(xhr.responseText));
                }
            } else {
                if (error) {
                    error();
                }
            }
        }
    }
}
</script>
```
控制台效果

<br>
就先写到这 累了 写不动了

## END
本文中的源码已提交Github
[https://github.com/zzzmhcn/jsp-demo](https://github.com/zzzmhcn/jsp-demo)
**参考**
[https://www.cnblogs.com/maoshine/p/17620190.html](https://www.cnblogs.com/maoshine/p/17620190.html)
[https://stackoverflow.com/questions/35606551/jstl-localdatetime-format](https://stackoverflow.com/questions/35606551/jstl-localdatetime-format)