Java Web开发:JSP、Servlet与相关技术深度解析
1. 使用JSP与JavaBeans
在Java Web开发中,JavaBeans与JSP的结合使用是一项重要技术。
1.1 JavaBean的基本要求
一个类要成为JavaBean,需要满足以下基本要求:
1. 拥有无参(空)构造函数。
2. 没有公共实例变量(字段)。
3. 通过名为getXxx(或isXxx)和setXxx的方法访问持久值。
1.2 JavaBean的基本使用
在JSP中使用JavaBean,主要有以下几种方式:
-
<jsp:useBean id="name" class="package.Class" />
:用于声明并实例化一个JavaBean。
-
<jsp:getProperty name="name" property="property" />
:用于获取JavaBean的属性值。
-
<jsp:setProperty name="name" property="property" value="value" />
:用于设置JavaBean的属性值,其中value属性可以接受JSP表达式。
1.3 将属性与请求参数关联
可以将JavaBean的属性与请求参数进行关联,具体方式如下:
-
单个属性
:
<jsp:setProperty
name="entry"
property="numItems"
param="numItems" />
- 自动类型转换 :对于基本类型,会根据包装类的valueOf方法进行自动类型转换。
- 所有属性 :
<jsp:setProperty name="entry" property="*" />
1.4 共享JavaBean:jsp:useBean的scope属性
jsp:useBean
的
scope
属性用于指定JavaBean的共享范围,常见的取值有:
| 范围 | 描述 |
| ---- | ---- |
| page | 默认值,表示除了绑定到局部变量外,Bean对象应在当前请求期间放置在PageContext对象中。 |
| application | 表示除了绑定到局部变量外,Bean将存储在通过预定义的application变量或调用getServletContext()可访问的共享ServletContext中。 |
| session | 表示除了绑定到局部变量外,Bean将存储在与当前请求关联的HttpSession对象中,可以使用getValue方法检索。 |
| request | 表示除了绑定到局部变量外,Bean对象应在当前请求期间放置在ServletRequest对象中,可以通过getAttribute方法访问。 |
1.5 条件性Bean创建
<jsp:useBean>
元素只有在找不到具有相同id和范围的Bean时才会实例化一个新的Bean。如果找到具有相同id和范围的Bean,则将现有的Bean绑定到id引用的变量。可以使
<jsp:setProperty>
语句依赖于新Bean的创建:
<jsp:useBean ...>
statements
</jsp:useBean>
2. 创建自定义JSP标签库
自定义JSP标签库可以提高代码的复用性和可维护性。
2.1 标签处理类
要创建自定义标签库,需要实现Tag接口,通常通过扩展TagSupport(无标签体或标签体逐字包含)或BodyTagSupport(操作标签体)来实现。标签处理类有以下几个重要方法:
-
doStartTag
:在标签开始时运行的代码。
-
doEndTag
:在标签结束时运行的代码。
-
doAfterBody
:处理标签体的代码。
2.2 标签库描述符文件
标签库描述符文件(TLD)在
taglib
元素中为每个标签处理程序包含一个
tag
元素。例如:
<tag>
<name>prime</name>
<tagclass>coreservlets.tags.PrimeTag</tagclass>
<info>Outputs a random N-digit prime.</info>
<bodycontent>EMPTY</bodycontent>
<attribute>
<name>length</name>
<required>false</required>
</attribute>
</tag>
2.3 JSP文件
在JSP文件中使用自定义标签库,需要进行以下操作:
- 使用
<%@ taglib uri="some-taglib.tld" prefix="prefix" %>
指令引入标签库。
- 使用
<prefix:tagname />
或
<prefix:tagname>body</prefix:tagname>
来使用标签。
2.4 为标签分配属性
- 标签处理类 :为每个属性xxx实现setXxx方法。
-
标签库描述符
:在
tag元素中添加attribute元素来定义属性。
<tag>
...
<attribute>
<name>length</name>
<required>false</required>
<rtexprvalue>true</rtexprvalue> <!-- sometimes -->
</attribute>
</tag>
2.5 包含标签体
-
标签处理类
:从
doStartTag方法返回EVAL_BODY_INCLUDE而不是SKIP_BODY。 -
标签库描述符
:在
tag元素中设置<bodycontent>JSP</bodycontent>。
2.6 可选地包含标签体
标签处理类可以根据请求时间参数的值,在不同时间返回
EVAL_BODY_INCLUDE
或
SKIP_BODY
。
2.7 操作标签体
要操作标签体,需要扩展BodyTagSupport类,实现
doAfterBody
方法,调用
getBodyContent
方法获取描述标签体的BodyContent对象。BodyContent有三个关键方法:
getEnclosingWriter
、
getReader
和
getString
。从
doAfterBody
方法返回
SKIP_BODY
。
2.8 多次包含或操作标签体
要再次处理标签体,从
doAfterBody
方法返回
EVAL_BODY_TAG
;要完成处理,返回
SKIP_BODY
。
2.9 使用嵌套标签
嵌套标签可以使用
findAncestorWithClass
方法找到它们嵌套的标签,并将数据放置在封闭标签的字段中。在标签库描述符中,无论实际页面中的嵌套结构如何,都应单独声明所有标签。
3. 集成Servlet和JSP
Servlet和JSP的集成是Java Web开发中的常见模式。
3.1 整体流程
- Servlet处理初始请求,读取参数、cookie、会话信息等。
- Servlet进行所需的计算和数据库查询。
- Servlet将数据存储在JavaBean中。
- Servlet将请求转发到多个可能的JSP页面之一,以呈现最终结果。
- JSP页面从JavaBean中提取所需的值。
3.2 请求转发语法
String url = "/path/presentation1.jsp";
RequestDispatcher dispatcher =
getServletContext().getRequestDispatcher(url);
dispatcher.forward();
3.3 转发到常规HTML页面
- 如果初始Servlet仅处理GET请求,则无需更改。
- 如果初始Servlet处理POST请求,则将目标页面从SomePage.html更改为SomePage.jsp,以便它也能处理POST请求。
3.4 设置全局共享Bean
- 初始Servlet :
Type1 value1 = computeValueFromRequest(request);
getServletContext().setAttribute("key1", value1);
- 最终JSP文档 :
<jsp:useBean id="key1" class="Type1" scope="application" />
3.5 设置会话Bean
- 初始Servlet :
Type1 value1 = computeValueFromRequest(request);
HttpSession session = request.getSession(true);
session.putValue("key1", value1);
- 最终JSP文档 :
<jsp:useBean id="key1" class="Type1" scope="session" />
3.6 解释目标页面中的相对URL
转发请求时使用原始Servlet的URL。浏览器不知道实际URL,因此它将相对于原始Servlet的URL解析相对URL。
3.7 通过替代方式获取RequestDispatcher(仅适用于2.2版本)
- 按名称:使用ServletContext的getNamedDispatcher方法。
- 相对于初始Servlet位置的路径:使用HttpServletRequest的getRequestDispatcher方法,而不是ServletContext的方法。
3.8 包含静态或动态内容
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.println("...");
RequestDispatcher dispatcher =
getServletContext().getRequestDispatcher("/path/resource");
dispatcher.include(request, response);
out.println("...");
JSP等效的是
<jsp:include>
,而不是JSP包含指令。
3.9 从JSP页面转发请求
<jsp:forward page="Relative URL" />
4. 使用HTML表单
HTML表单是Web应用程序中收集用户输入的重要方式。
4.1 FORM元素
通常的形式为:
<FORM ACTION="URL" ...> ... </FORM>
常见属性有:ACTION(必需)、METHOD、ENCTYPE、TARGET、ONSUBMIT、ONRESET、ACCEPT、ACCEPT - CHARSET。
4.2 文本字段
通常的形式为:
<INPUT TYPE="TEXT" NAME="..." ...>
常见属性有:NAME(必需)、VALUE、SIZE、MAXLENGTH、ONCHANGE、ONSELECT、ONFOCUS、ONBLUR、ONKEYDOWN、ONKEYPRESS、ONKEYUP。不同浏览器对于在文本字段中按Enter键提交表单的规则不同,因此应包含一个明确提交表单的按钮或图像映射。
4.3 密码字段
通常的形式为:
<INPUT TYPE="PASSWORD" NAME="..." ...>
常见属性与文本字段类似,但包含密码字段的表单应始终使用POST方法。
4.4 文本区域
通常的形式为:
<TEXTAREA NAME="..." ROWS=xxx COLS=yyy> ...
Some text
</TEXTAREA>
常见属性有:NAME(必需)、ROWS(必需)、COLS(必需)、WRAP(非标准)、ONCHANGE、ONSELECT、ONFOCUS、ONBLUR、ONKEYDOWN、ONKEYPRESS、ONKEYUP。初始文本中的空白会被保留,起始和结束标签之间的HTML标记会按字面意思处理,除了字符实体如
<
、
©
等。
4.5 提交按钮
通常的形式为:
<INPUT TYPE="SUBMIT" ...>
常见属性有:NAME、VALUE、ONCLICK、ONDBLCLICK、ONFOCUS、ONBLUR。当点击提交按钮时,表单会发送到FORM元素的ACTION参数指定的Servlet或其他服务器端程序。
4.6 替代按钮
- 替代提交按钮 :
<BUTTON TYPE="SUBMIT" ...>
HTML Markup
</BUTTON>
仅适用于Internet Explorer。
-
重置按钮
:
<INPUT TYPE="RESET" ...>
常见属性有:VALUE、NAME、ONCLICK、ONDBLCLICK、ONFOCUS、ONBLUR,除了VALUE属性外,其他属性仅用于JavaScript。
-
替代重置按钮
:
<BUTTON TYPE="RESET" ...>
HTML Markup
</BUTTON>
仅适用于Internet Explorer。
-
JavaScript按钮
:
<INPUT TYPE="BUTTON" ...>
常见属性有:NAME、VALUE、ONCLICK、ONDBLCLICK、ONFOCUS、ONBLUR。
-
替代JavaScript按钮
:
<BUTTON TYPE="BUTTON" ...>
HTML Markup
</BUTTON>
仅适用于Internet Explorer。
4.7 复选框
通常的形式为:
<INPUT TYPE="CHECKBOX" NAME="..." ...>
常见属性有:NAME(必需)、VALUE、CHECKED、ONCLICK、ONFOCUS、ONBLUR。只有当复选框被选中时,才会传输名称/值对。
4.8 单选按钮
通常的形式为:
<INPUT TYPE="RADIO" NAME="..." VALUE="..." ...>
常见属性有:NAME(必需)、VALUE(必需)、CHECKED、ONCLICK、ONFOCUS、ONBLUR。通过为一组单选按钮提供相同的NAME来表示它们属于同一组。
4.9 组合框
通常的形式为:
<SELECT NAME="Name" ...>
<OPTION VALUE="Value1">Choice 1 Text
<OPTION VALUE="Value2">Choice 2 Text
...
<OPTION VALUE="ValueN">Choice N Text
</SELECT>
SELECT元素的常见属性有:NAME(必需)、SIZE、MULTIPLE、ONCLICK、ONFOCUS、ONBLUR、ONCHANGE;OPTION元素的常见属性有:SELECTED、VALUE。
4.10 文件上传控件
通常的形式为:
<INPUT TYPE="FILE" ...>
常见属性有:NAME(必需)、VALUE(忽略)、SIZE、MAXLENGTH、ACCEPT、ONCHANGE、ONSELECT、ONFOCUS、ONBLUR(非标准)。在FORM声明中应使用
ENCTYPE="multipart/form-data"
。
4.11 服务器端图像映射
通常的形式为:
<INPUT TYPE="IMAGE" ...>
常见属性有:NAME(必需)、SRC、ALIGN。也可以为
<A HREF...>
元素内的标准IMG元素提供ISMAP属性。
4.12 隐藏字段
通常的形式为:
<INPUT TYPE="HIDDEN" NAME="..." VALUE="...">
常见属性有:NAME(必需)、VALUE。
4.13 Internet Explorer特性
- FIELDSET(带有LEGEND):用于分组控件。
- TABINDEX:用于控制制表顺序。这两个功能都是HTML 4.0规范的一部分,但Netscape 4不支持。
5. 使用小程序作为Servlet前端
小程序可以作为Servlet的前端,实现数据的交互。
5.1 使用GET发送数据并显示结果页面
String someData =
name1 + "=" + URLEncoder.encode(val1) + "&" +
name2 + "=" + URLEncoder.encode(val2) + "&" +
...
nameN + "=" + URLEncoder.encode(valN);
try {
URL programURL = new URL(baseURL + "?" + someData);
getAppletContext().showDocument(programURL);
} catch(MalformedURLException mue) { ... }
5.2 使用GET发送数据并直接处理结果(HTTP隧道)
graph TD;
A[创建URL对象] --> B[创建URLConnection对象];
B --> C[禁止浏览器缓存数据];
C --> D[设置HTTP头部];
D --> E[创建输入流];
E --> F[读取文档每一行];
F --> G[关闭输入流];
具体步骤如下:
1. 创建一个引用小程序主机的URL对象,通常根据小程序加载的主机名构建URL。
URL currentPage = getCodeBase();
String protocol = currentPage.getProtocol();
String host = currentPage.getHost();
int port = currentPage.getPort();
String urlSuffix = "/servlet/SomeServlet";
URL dataURL = new URL(protocol, host, port, urlSuffix);
- 创建一个URLConnection对象。
URLConnection connection = dataURL.openConnection();
- 指示浏览器不要缓存URL数据。
connection.setUseCaches(false);
- 设置所需的HTTP头部。
connection.setRequestProperty("header", "value");
- 创建一个输入流,常见的是BufferedReader。
BufferedReader in =
new BufferedReader(new InputStreamReader(
connection.getInputStream()));
- 读取文档的每一行,直到读取到null。
String line;
while ((line = in.readLine()) != null) {
doSomethingWith(line);
}
- 关闭输入流。
in.close();
5.3 发送序列化数据
- 小程序代码 :
graph TD;
A[创建URL对象] --> B[创建URLConnection对象];
B --> C[禁止浏览器缓存数据];
C --> D[设置HTTP头部];
D --> E[创建ObjectInputStream];
E --> F[读取数据结构];
F --> G[关闭输入流];
具体步骤如下:
1. 创建一个引用小程序主机的URL对象。
URL currentPage = getCodeBase();
String protocol = currentPage.getProtocol();
String host = currentPage.getHost();
int port = currentPage.getPort();
String urlSuffix = "/servlet/SomeServlet";
URL dataURL = new URL(protocol, host, port, urlSuffix);
- 创建一个URLConnection对象。
URLConnection connection = dataURL.openConnection();
- 指示浏览器不要缓存URL数据。
connection.setUseCaches(false);
- 设置所需的HTTP头部。
connection.setRequestProperty("header", "value");
- 创建一个ObjectInputStream。
ObjectInputStream in =
new ObjectInputStream(connection.getInputStream());
- 使用readObject方法读取数据结构,并进行类型转换。
SomeClass value = (SomeClass)in.readObject();
doSomethingWith(value);
- 关闭输入流。
in.close();
- Servlet代码 :
graph TD;
A[指定二进制内容类型] --> B[创建ObjectOutputStream];
B --> C[写入数据结构];
C --> D[刷新流];
具体步骤如下:
1. 指定发送的是二进制内容,将响应的MIME类型设置为
application/x-java-serialized-object
。
String contentType =
"application/x-java-serialized-object";
response.setContentType(contentType);
- 创建一个ObjectOutputStream。
ObjectOutputStream out =
new ObjectOutputStream(response.getOutputStream());
- 使用writeObject方法写入数据结构,自定义类需要实现Serializable接口。
SomeClass value = new SomeClass(...);
out.writeObject(value);
- 刷新流,确保所有内容都已发送到客户端。
out.flush();
5.4 使用POST发送数据并直接处理结果(HTTP隧道)
graph TD;
A[创建URL对象] --> B[创建URLConnection对象];
B --> C[禁止浏览器缓存数据];
C --> D[允许发送数据];
D --> E[创建ByteArrayOutputStream];
E --> F[附加输出流];
F --> G[将数据放入缓冲区];
G --> H[设置Content-Length头部];
H --> I[设置Content-Type头部];
I --> J[发送实际数据];
J --> K[打开输入流];
K --> L[读取结果];
具体步骤如下:
1. 创建一个引用小程序主机的URL对象。
URL currentPage = getCodeBase();
String protocol = currentPage.getProtocol();
String host = currentPage.getHost();
int port = currentPage.getPort();
String urlSuffix = "/servlet/SomeServlet";
URL dataURL =
new URL(protocol, host, port, urlSuffix);
- 创建一个URLConnection对象。
URLConnection connection = dataURL.openConnection();
- 指示浏览器不要缓存结果。
connection.setUseCaches(false);
- 告诉系统允许发送数据。
connection.setDoOutput(true);
- 创建一个ByteArrayOutputStream来缓冲要发送到服务器的数据。
ByteArrayOutputStream byteStream =
new ByteArrayOutputStream(512);
- 将输出流附加到ByteArrayOutputStream,使用PrintWriter发送普通表单数据,使用ObjectOutputStream发送序列化数据结构。
PrintWriter out = new PrintWriter(byteStream, true);
- 将数据放入缓冲区,使用print方法发送表单数据,使用writeObject方法发送高级序列化对象。
String val1 = URLEncoder.encode(someVal1);
String val2 = URLEncoder.encode(someVal2);
String data = "param1=" + val1 +
"¶m2=" + val2;
out.print(data);
out.flush();
- 设置Content-Length头部。
connection.setRequestProperty
("Content-Length", String.valueOf(byteStream.size()));
-
设置Content-Type头部,对于普通表单数据,应显式设置为
application/x-www-form-urlencoded,对于序列化数据,该值无关紧要。
connection.setRequestProperty
("Content-Type", "application/x-www-form-urlencoded");
- 发送实际数据。
byteStream.writeTo(connection.getOutputStream());
- 打开输入流,通常使用BufferedReader处理ASCII或二进制数据,使用ObjectInputStream处理序列化Java对象。
BufferedReader in =
new BufferedReader(new InputStreamReader
(connection.getInputStream()));
- 读取结果,具体细节取决于服务器发送的数据类型。
String line;
while((line = in.readLine()) != null) {
doSomethingWith(line);
}
5.5 绕过HTTP服务器
小程序可以使用以下方式直接与它们的主机服务器通信:
- 原始套接字
- 带有对象流的套接字
- JDBC
- RMI
- 其他网络协议
Java Web开发:JSP、Servlet与相关技术深度解析
6. 技术对比与总结
在Java Web开发中,JSP、Servlet、JavaBeans、自定义标签库、HTML表单以及小程序前端等技术各自发挥着重要作用,下面对它们进行对比总结。
| 技术 | 主要用途 | 优点 | 缺点 |
|---|---|---|---|
| JSP与JavaBeans | 动态生成网页内容,分离业务逻辑和表现层 | 代码复用性高,易于维护,可实现数据的封装和共享 | 可能导致JSP页面代码臃肿,尤其是包含大量脚本时 |
| 自定义JSP标签库 | 提高代码复用性和可维护性,封装复杂逻辑 | 使JSP页面更简洁,增强代码可读性 | 开发和维护标签库需要一定的技术成本 |
| 集成Servlet和JSP | 实现业务逻辑处理和页面呈现的分离 | 遵循MVC架构,便于团队协作开发,提高代码可维护性 | 开发流程相对复杂,需要理解Servlet和JSP的交互机制 |
| HTML表单 | 收集用户输入信息 | 简单易用,是Web应用中常见的交互方式 | 安全性较低,需要进行额外的验证和防护 |
| 小程序作为Servlet前端 | 实现客户端和服务器端的数据交互 | 可以提供丰富的用户界面和交互体验 | 小程序的兼容性和性能可能受到浏览器和设备的限制 |
7. 实际应用案例分析
为了更好地理解上述技术在实际中的应用,下面通过一个简单的用户注册系统案例进行分析。
7.1 需求分析
用户可以在注册页面输入用户名、密码、邮箱等信息,点击注册按钮后,系统将用户信息保存到数据库中,并显示注册成功页面。
7.2 技术选型
- 使用HTML表单收集用户输入信息。
- 使用Servlet处理用户注册请求,验证用户信息,并将数据保存到数据库中。
- 使用JSP和JavaBeans实现页面的动态生成和数据的封装。
7.3 详细实现步骤
- 创建HTML注册表单页面(register.html)
<!DOCTYPE html>
<html>
<head>
<title>用户注册</title>
</head>
<body>
<form action="registerServlet" method="post">
<label for="username">用户名:</label>
<input type="text" id="username" name="username" required><br>
<label for="password">密码:</label>
<input type="password" id="password" name="password" required><br>
<label for="email">邮箱:</label>
<input type="email" id="email" name="email" required><br>
<input type="submit" value="注册">
</form>
</body>
</html>
- 创建JavaBean类(User.java)
import java.io.Serializable;
public class User implements Serializable {
private String username;
private String password;
private String email;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
- 创建Servlet类(RegisterServlet.java)
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/registerServlet")
public class RegisterServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取用户输入信息
String username = request.getParameter("username");
String password = request.getParameter("password");
String email = request.getParameter("email");
// 创建User对象
User user = new User();
user.setUsername(username);
user.setPassword(password);
user.setEmail(email);
// 模拟将用户信息保存到数据库
// 这里可以添加实际的数据库操作代码
// 将User对象存储到request中
request.setAttribute("user", user);
// 转发到注册成功页面
request.getRequestDispatcher("registerSuccess.jsp").forward(request, response);
}
}
- 创建JSP注册成功页面(registerSuccess.jsp)
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="com.example.User" %>
<!DOCTYPE html>
<html>
<head>
<title>注册成功</title>
</head>
<body>
<h1>注册成功!</h1>
<p>用户名:<jsp:getProperty name="user" property="username" /></p>
<p>邮箱:<jsp:getProperty name="user" property="email" /></p>
</body>
</html>
7.4 案例总结
通过这个案例可以看到,HTML表单用于收集用户输入,Servlet处理业务逻辑,JavaBeans封装用户数据,JSP页面显示注册结果,实现了业务逻辑和表现层的分离,提高了代码的可维护性和可扩展性。
8. 技术发展趋势与展望
随着互联网技术的不断发展,Java Web开发技术也在不断演进。以下是一些可能的发展趋势:
- 微服务架构 :将大型Web应用拆分成多个小型、自治的服务,每个服务专注于单一业务功能,提高开发效率和系统的可伸缩性。
- 前后端分离 :前端使用现代前端框架(如Vue.js、React.js)构建用户界面,后端使用Servlet、Spring等框架处理业务逻辑,通过RESTful API进行数据交互,提高开发效率和用户体验。
- 容器化和云原生 :使用Docker容器化应用,利用Kubernetes进行容器编排和管理,实现应用的快速部署和弹性伸缩。
- 人工智能和大数据 :将人工智能和大数据技术应用到Java Web开发中,如智能推荐系统、数据分析和可视化等,为用户提供更个性化的服务。
在未来的Java Web开发中,我们需要不断学习和掌握新的技术,结合实际项目需求,灵活运用各种技术,以开发出更高效、更稳定、更具创新性的Web应用。
9. 学习建议与资源推荐
对于想要深入学习Java Web开发的开发者,以下是一些学习建议和资源推荐。
9.1 学习建议
- 理论与实践结合 :不仅要学习理论知识,还要通过实际项目进行实践,加深对知识的理解和掌握。
- 阅读优秀代码 :学习开源项目和优秀的代码示例,了解他人的编程思路和最佳实践。
- 参与技术社区 :加入技术社区,与其他开发者交流经验,分享学习心得,及时了解行业动态。
- 持续学习 :Java Web技术不断发展,要保持学习的热情,不断更新自己的知识体系。
9.2 资源推荐
- 书籍 :《Effective Java》《Java核心技术》《Servlet与JSP核心编程》等。
- 在线课程 :慕课网、网易云课堂、Coursera等平台上有很多优质的Java Web开发课程。
- 开源项目 :GitHub上有许多优秀的Java Web开源项目,可以参考学习。
- 技术博客 :InfoQ、开源中国、博客园等网站上有很多关于Java Web开发的技术文章和经验分享。
通过不断学习和实践,相信你能够掌握Java Web开发的核心技术,成为一名优秀的Java Web开发者。
超级会员免费看

被折叠的 条评论
为什么被折叠?



