关于2023年如何开发一个Java JSP JSTL项目指南 home 编辑时间 2023/11/15 ![](/api/file/getImage?fileId=6555abb8da7405001400deba) ## 前言 众所周知 `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> 就先写到这 累了 写不动了 ![](https://leanote.zzzmh.cn/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) 送人玫瑰,手留余香 赞赏 Wechat Pay Alipay Springboot 3.x 项目使用 JSP 笔记 Leanote 蚂蚁笔记 Docker从零部署安装 2023