关于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`,右边如下图
![](/api/file/getImage?fileId=65547fb5da7405001400dda9)
版本选择最新的 `Jakarta EE 10` 其余默认
![](/api/file/getImage?fileId=65548007da7405001400ddab)
开局发现居然是个 `Maven` 项目
而且已经写好了示例代码
![](/api/file/getImage?fileId=6554817cda7405001400ddaf)
结果一运行 404
![](/api/file/getImage?fileId=655580bbda7405001400de68)
简单修改一下 `Tomcat` 的配置
![](/api/file/getImage?fileId=6554817cda7405001400ddb0)
![](/api/file/getImage?fileId=6554817cda7405001400ddae)
再次点击运行即可成功访问到demo
![](/api/file/getImage?fileId=655481bfda7405001400ddb1)
<br>
由于上次学 `JSP` 已经是10年前了,开发工具还是 `eclipse`,没有 `maven` 导包是手动下载jar包并粘贴到lib目录下,现在一下子过于与时俱进,稍微补了补课
![](/api/file/getImage?fileId=65548332da7405001400ddba)
<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可以看到执行结果 已成功直连数据库
![](/api/file/getImage?fileId=65557e03da7405001400de66)
这里有个小问题,时间格式不对,我试了用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发现时间已经显示正常了
![](/api/file/getImage?fileId=655586e6da7405001400de88)
同理自己实现一个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,一次跑通
![](/api/file/getImage?fileId=65559c64da7405001400deb0)
IDEA Console也能收到传入参数
![](/api/file/getImage?fileId=65559ca4da7405001400deb1)
顺手补上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>
```
控制台效果
![](/api/file/getImage?fileId=65559eedda7405001400deb2)
<br>
就先写到这 累了 写不动了
![](/api/file/getImage?fileId=64c9c860da74050014005b69)
## 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)