前言

本文章是根据法国国立高等电力技术、电子学、计算机、水力学与电信学校 (E.N.S.E.E.I.H.T.) 第八学期课程*“Application Web”* 总结而来的【部分课程笔记】。碍于本人学识有限,部分叙述难免存在纰漏,请读者注意甄别。

第一部分:Introduction

Web 基于 客户端-服务器 模型, 这里客户端是浏览器,服务器是 Web 服务器(如 Apache)。
主要包括三个方面:

1. 带有 URI/URN/URL 的文件的指定和位置

  • URI (Uniform Resource Identifier):统一资源标识符,就是在某一规则下能把一个资源独一无二地标识出来。URI=URL    URNURI = URL \; \cup \; URN
  • URL (Uniform Resource Locator):统一资源定位符。标识一个互联网资源,并指定对其进行操作或获取该资源的方法。
  • URN (Uniform Resource Name):统一资源名称。用于标识唯一书目的 ISBN系统是一个典型的 URN 使用范例

2. 使用 HTML 和 CSS 语言对文档进行编码

HTML

HTML (HyperText Markup Language) 语言由**包含内容的标签 (composé de balises)**组成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!DOCTYPE html>
<html> <!-- 标记 HTML文档 -->

<head> <!-- 标记 HTML头部元素 -->
<meta charset="utf-8"> <!-- 标记 HTML元数据 -->
<title>标题</title> <!-- 标记 HTML的标题 -->
</head>

<body> <!-- 标记 HTML文档的主体 -->
<h1>我的第一个标题</h1> <!-- 标记 标题 -->
<hr> <!-- 标记 水平线 -->
<p>这是一个段落。</p> <!-- 标记 一个段落 -->
<br> <!-- 标记 换行 -->
<em></em> <!-- 标记 着重元素(加粗) -->
<div> <!-- 内容划分元素 -->
<table> <!-- 表格 -->
<td></td> <!-- table data cell 表格中的一个单元格 -->
<th></th> <!-- table header cell 表格中的表头 -->
<tr></tr> <!-- table row 表格中的一行 -->
</table>
<li></li> <!-- List Item 列表项目 -->
<nl></nl> <!-- navigation lists 导航列表 -->
<ol></ol> <!-- Ordered List 排序列表 -->
<ul></ul> <!-- Unordered List 不排序列表 -->
<form></form>
<!-- 表示文档中的一个区域,此区域包含交互控件,用于向 Web 服务器提交信息 -->
<a href="https://www.google.com">这是一个链接</a>
<!-- 标签<a>: 设置超文本链接 -->
<img src="/images/logo.png" width="258" height="39" />
<!-- 标签<img>: 设置图片 -->
</body>

</html>
CSS

层叠样式表Cascading Style Sheets) 是一种用来为结构化文档(如HTML文档或XML应用)添加样式(字体、间距和颜色等)的计算机语言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
body {
background-color:#d0e4fe;
}
h1 {
color:orange;
text-align:center;
}
p {
font-family:"Times New Roman";
font-size:20px;
}
/* Exemple: <h2 id="boldFond">加粗文本</h2>*/
.boldFond {
font_weight: bold;
}
  • 如何在 HTTP 文件里使用 CSS文件
1
2
3
<head>
<link rel="stylesheet" type="text/css" herf="/style.css"/>
</head>

3. 浏览器和 Web 服务器之间的通信协议 (HTTP)

HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网服务器传输超文本到本地浏览器的传送协议,基于TCP/IP通信协议来传递数据(HTML 文件, 图片文件, 查询结果等)。面向连接,安全。

HTTP协议工作于 C/S 架构为上。浏览器作为HTTP客户端通过URL向HTTP服务端即WEB服务器发送所有请求。Web服务器根据接收到的请求后,向客户端发送响应信息

graph LR
Client --Request--> Server
Server --Reponse--> Client

HTTP使用URL来传输数据和建立连接

1
2
http://	127.0.0.1		:8080	/enseeiht/	index.html	?name=dai&age=18	#detail
协议 IP地址或DNS 端口 虚拟目录 文件名 参数部分 锚
HTTP 请求数据格式
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 请求行 -->
POST /example/index.html HTTP/1.1
<!-- 请求方法 | 请求资源路径 | 协议及版本 -->

<!-- 请求头 key: value -->
Host: www.google.com <!-- 请求主机名 -->
User_Agent: Chrome <!-- 请求的浏览器信息 -->
Accept: xxx/xxx <!-- 浏览器能接收的资源类型,如text/*、image/* -->
Cookie: NAME=VALUE <!-- 上报 Cookie -->
...

<!-- 请求体 -->
username=xxx&password=123456

json 格式的 HTTP 请求数据

1
2
3
4
POST http://www.example.com HTTP/1.1 
Content-Type: application/json;charset=utf-8

{"username":"xxx", "password":"123456", "age": 24, "hobby":["x","xx","xxx"]}

xml 格式的 HTTP 请求数据

1
2
3
4
5
6
7
8
POST http://www.example.com HTTP/1.1 
Content-Type: text/xml

<?xml version="1.0" encoding="UTF-8">
<REQ>
<username>xxx</username>
<passowrd>123456</password>
</REQ>
HTTP 的请求方法
  • GET:请求指定的页面信息,并返回实体主体。

    请求行中请求 (/example/index.html?username=xxx&password=123456),有长度限制

  • HEAD:类似于 GET 请求,只不过返回的响应中没有具体的内容,用于获取报头

  • POST:向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST 请求可能会导致新的资源的建立和/或已有资源的修改。请求体中请求,没有长度限制

  • PUT:从客户端向服务器传送的数据取代指定的文档的内容。

  • DELETE:请求服务器删除指定的页面。

  • CONNECT:HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。

  • OPTIONS:允许客户端查看服务器的性能。

  • TRACE:回显服务器收到的请求,主要用于测试或诊断。

HTTP 响应数据格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- 响应行 -->
HTTP/1.1 200 OK
<!-- 协议及版本 响应状态 状态码描述 -->

<!-- 响应头 key: value -->
Server: www.google.com <!-- 响应主机名 -->
Content-Type: text/html <!-- 该响应的内容类型,text/html、image/jpg -->
Content-Length: <!-- 该响应的内容长度(字节数) -->
Content-Encoding: <!-- 该响应的编码 -->
Cache-Control:<!-- 客户端如何缓存该响应 -->
Set-Cookie: NAME=VALUE, expires=过期时间戳, ... <!-- 下放 Cookie -->
...

<!-- 响应体 -->
<http>
<head>
<title></title>
</head>
<body></body>
</http>
HTTP 状态码

状态代码有三位数字组成,第一个数字定义了响应的类别,共分五种类别:

  • 1xx:指示信息。表示请求已接收,继续处理
  • 2xx:成功。表示请求已被成功接收、理解、接受
  • 3xx:重定向。要完成请求必须进行更进一步的操作
  • 4xx:客户端错误。请求有语法错误或请求无法实现
  • 5xx:服务器端错误。服务器未能实现合法的请求

常见状态码:

1
2
3
4
5
6
7
8
200 OK                        //客户端请求成功
302 Found //资源已移动到Location响应头指定的URL,浏览器自动重新跳转
400 Bad Request //客户端请求有语法错误,不能被服务器所理解
401 Unauthorized //请求未经授权
403 Forbidden //服务器收到请求,但是拒绝提供服务
404 Not Found //请求资源不存在,eg:输入了错误的URL
500 Internal Server Error //服务器发生不可预期的错误
503 Server Unavailable //服务器当前不能处理客户端的请求,一段时间后可能恢复正常

4. HTTP 表单 (formulaire)

网站怎样与用户进行交互?答案是使用HTML表单(form)。表单是可以把浏览者输入的数据传送到服务器端,这样服务器端程序就可以处理表单传过来的数据。

1
2
3
4
<form action="url" method="method">
<!-- action: 规定当提交表单时向何处发送表单数据
method: 规定用于发送 form-data 的 HTTP 方法(GET/POST)-->
</form>
getpost的差别

本质上的区别是语义的区别,根据HTTP规范,POST 的语义是根据请求负荷(报文主体)对指定的资源做出处理。POST 方法是安全、不可缓存的。GET 的语义是请求获取指定的资源,具体的处理方式视资源类型而不同。GET 不安全,可缓存。具体差别如下:

  • get在后退刷新时是无害的,post会重新提交请求;
  • get参数通过URL传递,post放在Request body中;
  • get请求参数保留在浏览器历史记录中,post参数不会保留;
  • get产生的URL地址可以被存为书签,而post不可以;
  • 对参数的数据类型,get只接受ASCII字符,而post没有限制;
  • get比post更不安全,因为发送的数据显示在URL上,在发送密码或其他敏感信息时绝不要使用get;
  • get请求只能进行url编码,而post支持多种编码方式。
表单元素:input
1
<input type="type" name="input" size="10pd" value="value">
常见的 input 标签:
  • submit 用于数据提交

  • text 可输入文本

  • password 用于输入密码,输入内容会呈现为小圆点,进行隐藏

  • checkbox 多选框

  • radio 多选框

  • select 下拉选择 并和 option 标签一起使用

    1
    2
    3
    4
    5
    <select name="choice">
    <option value="value1">选择1</option>
    <option value="value2">选择2</option>
    <option value="value3">选择3</option>
    </select>
  • file 上传文件

  • hidden 隐藏组件

  • bottom 按钮

  • reset 重置表单

  • textarea 文本区域

    1
    2
    3
    <textarea name="teatarea" rows="4" cols="4">
    这是文本区域,可以键入
    </textarea>

5. CGI的工作原理

CGI(Common Gateway Interface)公共网关接口,根据CGI标准,编写外部扩展应用程序,可以对客户端浏览器输入的数据进行处理,完成客户端与服务器的交互操作。CGI规范定义了Web服务器如何向扩展应用程序发送消息,在收到扩展应用程序的信息后又如何进行处理等内容。

CGI 应用程序能与浏览器进行交互,还可通过数据API与数据库服务器等外部数据源进行通信,从数据库服务器中获取数据。格式化为HTML文档后,发送给浏览器,也可以将从浏览器获得的数据放到数据库中。

graph RL

subgraph "C/S"
浏览器 --"get/post"--> Web服务器
Web服务器 --"generated.html"--> 浏览器
end

Web服务器 --stdin--> CGI
Web服务器 --$QUERY_STRING--> CGI
CGI --stdout--> Web服务器

subgraph "动态网页"
CGI --传入数据--> 程序
程序 --产生数据--> CGI
end

<form>标签的 METHOD 属性来决定具体使用哪一种方法。

  • METHOD=GET 时,向 CGI 传递表单编码信息的是通过命令来进行的。表单编码信息大多数是通过环境变量 QUERY_STRING 来传递的。
  • METHOD=POST,表单信息通过标准输入 stdin 来读取。

6. HTTP Cookies

Cookie 是一个保存在客户机中的简单的文本文件, 这个文件与特定的 Web 文档关联在一起, 保存了该客户机访问这个Web 文档时的信息, 当客户机再次访问这个 Web 文档时这些信息可供该文档使用

由服务器创建,保存在客户端中

组成

Cookie是一段不超过4KB的小型文本数据,由一个名称(Name)、一个值(Value)和其它几个用于控制Cookie有效期、安全性、使用范围的可选属性组成。其中:

  1. Name/Value:设置Cookie的名称及相对应的值,对于认证Cookie,Value值包括Web服务器所提供的访问令牌。
  2. Expires属性:设置Cookie的生存期。有两种存储类型的Cookie:会话性与持久性。Expires属性缺省时,为会话性Cookie,仅保存在客户端内存中,并在用户关闭浏览器时失效;持久性Cookie会保存在用户的硬盘中,直至生存期到或用户直接在网页中单击“注销”等按钮结束会话时才会失效 [3] 。
  3. Path 属性:定义了Web站点上可以访问该Cookie的目录。
  4. Domain 属性:指定了可以访问该 Cookie 的 Web 站点或域。Cookie 机制并未遵循严格的同源策略,允许一个子域可以设置或获取其父域的 Cookie。当需要实现单点登录方案时,Cookie 的上述特性非常有用,然而也增加了 Cookie受攻击的危险,比如攻击者可以借此发动会话定置攻击。因而,浏览器禁止在 Domain 属性中设置.org.com 等通用顶级域名、以及在国家及地区顶级域下注册的二级域名,以减小攻击发生的范围
  5. Secure 属性:指定是否使用 HTTPS 安全协议发送Cookie。使用HTTPS安全协议,可以保护Cookie在浏览器和Web服务器间的传输过程中不被窃取和篡改。该方法也可用于Web站点的身份鉴别,即在HTTPS的连接建立阶段,浏览器会检查Web网站的SSL证书的有效性。但是基于兼容性的原因(比如有些网站使用自签署的证书)在检测到SSL证书无效时,浏览器并不会立即终止用户的连接请求,而是显示安全风险信息,用户仍可以选择继续访问该站点。由于许多用户缺乏安全意识,因而仍可能连接到Pharming攻击所伪造的网站
  6. HTTPOnly 属性 :用于防止客户端脚本通过document.cookie属性访问Cookie,有助于保护Cookie不被跨站脚本攻击窃取或篡改。但是,HTTPOnly的应用仍存在局限性,一些浏览器可以阻止客户端脚本对Cookie的读操作,但允许写操作;此外大多数浏览器仍允许通过 XMLHTTP 对象读取 HTTP 响应中的Set-Cookie头 。

第二部分:动态网页

2.1 Servlet

Servlet是 Java 提供的一门动态web资源开发技术。是JavaEE 的规范之一,其实就是一个接口,我们需要定义 Servlet 的类实现 Servlet接口。

1
2
3
4
5
6
7
interface Servlet{
void init(ServletConfig config) throws ServletException;
void destory() throws ServletException;
ServletConfig getServletConfig() throws ServletException;
String getServletInfo() throws ServletException;
void service(ServletRequest req, ServletResponse res) throws ServletException;
}

但是在上述 Servlet 接口中只有 void service(ServletRequest req, ServletResponse res) 方法最为常用,我们将 Servlet 接口封装为 HttpServlet 抽象类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public abstract class HttpServlet extends GenericServlet {
/* Provides an abstract class to be subclassed to create an HTTP servlet suitable for a Web site. A subclass of HttpServlet must override at least one method, usually one of these:*/
HttpServletRequest req;
HttpServletResponse res;

protected void init();

// if the servlet supports HTTP GET requests
protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException;

// for HTTP POST requests
protected void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException;
...
}
Request 和 Response
2022-05-06 11.28.18
Request 获取请求数据

请求数据分为3部分:

  1. 请求行:GET /request-demo/req1?username=xxx HTTP/1.1
  • String getMethod():获取请求方式:GET/POST
  • String getContextPath():获取虛拟目录(项目访问路径):/request-demo
  • String Buffer getRequestURL():获取URL(统一资源定位符):http://localhost:8080/request-demo/req1
  • String getRequestURI():获取URI(统一资源标识符):/request-demo/req1
  • String getQueryString():获取请求参数(GET 方式): username=zhangsan&password=1
  1. 请求头:User-Agent: Mozilla/5.0 Chrome/91.0.4472.106
  • String getHeader(String name):根据请求头名称,获取值
  1. 请求体 (POST 方式):username=xxx&password=123456
  • ServletlnputStream getinputStream():获取字节输入流
  • BufferedReader getReader():获取字符输入流

getParameter():我们将 GET 方式中获取请求参数 String getQueryString()POST 方式中获取请求参数 BufferedReader getReader() 同一为一种通用的获取请求参数的方法:getParameter(key)。这其实是将所有的参数解析后存在在一个 MAP 中,通过 key 来获取 value

【请求转发】request.getRequestDispatcher("资源B的路径").forword(request,respond):如果服务器有两个资源,资源A 和资源B,资源A处理一部分后跳转 (forword) 到资源B继续处理。地址栏不变,一次请求

  • 请求转发资源间共享数据:使用 Request 对象
    • void setAttribute(String name, Object o):存储数据到 request 域中
    • Object getAttribute (String name):根据 key,获取值
    • void removeAttribute(String name):根据 key,删除该键值对
Response 设置响应数据

响应数据分为3部分:

  1. 响应行:HTTP/1.1 200 OK

    • void setStatus(int statusCode):设置响应状态码
  2. 响应头:Content-Type: text/html

    • void setHeader(String name, String value):设置响应头 键值对
  3. 响应体:<html></html>

  • Printwriter writer = response.getWriter():获取宇符输出流
    • writer.write("<html>")
  • Servletoutputstream outputStream = response.getoutputStream():获取字节输出流

【重定向 Redierct】当前资源A无法处理(statusCode=302),重定向到别的资源B (location) 处理。地址栏变化,两次不同的请求

1
2
3
4
response.sendRedierct("资源B的路径");
// 与以下代码等价
response.setStatus(302);
response.setHeader("location","资源B的路径");

例:

2022-05-03 20.48.15

web_app/demo.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<html>
<head>
<title>demo</title>
</head>

<body>
<h1>Enter a person</h1>
<form action= "web_app/demo" method="post">
<!-- action是请求的路径,规定当提交表单时向何处发送表单数据 -->
firstname<input type="text" name="firstname"><br/>
lastname<input type="text" name="lastname"><br/>
email<input type="text" name="email"><br/>
<input type="submit" name="op" value="add">
<input type="submit" name="op" value="list">
</form>
</body>
</html>
  • 由上述代码可见,action 指定的请求路径是 web_app/demo
  • method 指定的方法是 post

servletDemo.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 一个路径可以配置多个 urlPattern = {"/demo1", "/demo2"}
@WebServlet(urlPattern="/web_app/demo")
public class Directory extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
PrintWriter out = response.getWriter();
try {
// 对于数据库的操作
String db_url = "jdbc:hsqldb:hsql://localhost/web_app";
String db_user = "sa";
Class.forName("org.hsqldb.jdbcDriver");
Connection con = DriverManager.getConnection(db_url, db_user, null);

response.setContentType("text/html");
out.print("<html><head><title>demo</title></head>");
out.print("<body><h1>Enter a person</h1>");

String op = request.getParameter("op");
if (op.equals("list")) {
// op = list
Statement stmt = con.createStatement();
ResultSet res = stmt.executeQuery("SELECT * FROM demo");

while(res.next()) {
out.print("<p>"+res.getString("firstname")+" "+res.getString("lastname") +" "+res.getString("email")+"</p>");
}
} else {
// op = add
PreparedStatement ps = con.prepareStatement("INSERT INTO demo VALUES(?,?,?)");
ps.setString(1, request.getParameter("firstname"));
ps.setString(2, request.getParameter("lastname"));
ps.setString(3, request.getParameter("email"));
ps.executeUpdate();
out.print("Person was added");
}

out.print("</body></html>");

} catch (Exception ex) {
ex.printStackTrace(out);
}
}
}

客户端的会话构造技术:将数据保存在客户端,每次请求都携带 Cookies 数据进行访问。

  • Cookie 存活时间:
    • 默认情况下,Cookie 存储在浏览器内存中,当浏览器关闭,内存释放,则 Cookie 被销毁。
    • setMaxAge(int seconds) :设置存活时间。(+表示存入硬盘,到期删除;-表示默认情况;0表示删除对应Cookie)
2022-05-04 09.21.41
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 1.创建Cookie对象,设置数据
Cookie cookie = new Cookie(String name, String value);
void setValue(String newValue);
String getValue();
void setDomain(String pattern);
String getDomain();
void setMxaAge(int expiry);
int getMaxAge();

// 2.发送Cookie到客户端:使用response对象
HttpServletRequest.addCookie(cookie);

// 3.获取客户端携带的所有Cookie
Cookie[] cookies = HttpServletRequest.getCookie();
// 遍历所有cookies数组,获取每一个Cookie对象
for (Cookie c:cookies){
// 得到键 key
c.getName();
// 得到值 value
c.getValue();
}
Session

服务器端的会话跟踪技术:将数据保存在服务端。Session的是现实基于Cookies的。

2022-05-04 09.38.52

JavaEE 提供了 HttpSession 接口,来实现一次回话的多次请求间的数据共享功能

1
2
3
4
5
6
7
8
// 1. 获取 Session 对象
HttpSession session = HttpServletRequest.getSession;

// 2. Session 中的功能
void setAttribute(String name, Object o); // 将数据存储到 Session 中
Object getAttritube(String name); // 根据 key 获得 value
void removeAttritude(String name); // 根据 key 删除该键值对
...

2.2 JSP

Java Server Page,即Java服务端页面。是一种动态网页技术,既可以定义 HTML、JS、CSS 等静态内容,还可以定义 Java 代码的动态内容。JSP = HRTML + Java

1
2
3
4
5
6
7
8
9
10
11
12
<html> <!-- html 代码 -->
<head>
<title>Title</title>
</head>

<body>
<h1>JSP, Hello world.</h1>
<% // JSP 脚本:Java 代码
System.out.println("hello.jsp"); // console 输出
%>
</body>
</html>
JSP 原理

JSP 本质上是一个 Servlet。JSP 在被访问时,由 JSP 容器(Tomcat)将其转换为 Java 文件(Servlet),再由 JSP 容器将其编译,最终对外提供服务的就是这个字节码文件:

2022-05-04 10.05.07
JSP 脚本 (Scriptlet)

JSP 脚本用于在 JSP 页面内定义 Java 代码。

JSP 脚本的分类:

  • <% ... %>:Java 脚本。内容会直接放到 _jspService() 方法之中;
  • <%= ... %>:表达式。内容会直接放到 out.print() 方法之中,作为 out.print() 的参数;
  • <%! ... %>:声明。内容会放到 _jspService() 方法之外,被类直接包含。定义成员变量、方法等
1
2
3
4
5
<html>
<% System.out.println("Hello, JSP"); %>
<%= "hello" %>
<%! String name = 123; %>
</html>
1
2
3
4
5
6
7
8
9
10
public final class hello_jsp extends HttpJspBase implements JspSourceDependent {
String name = 123; // 成员变量或方法
...
public void _jspService(req, res) {
out.write("<html>");
System.out.println("Hello, JSP");
out.print("hello"); // 显示输出在页面上
out.write("<\html>");
}
}
JSP 的指令标签

JSP指令用来设置整个JSP页面相关的属性,如网页的编码方式和脚本语言。

语法格式如下:

1
<%@ directive attribute="value" %>

指令可以有很多个属性,它们以键值对的形式存在,并用逗号隔开。

JSP中的三种指令标签:

(1)<%@ page ... %>

Page指令为容器提供当前页面的使用说明。一个JSP页面可以包含多个page指令。

Page指令的语法格式:

1
<%@ page attribute="value" %>

等价的XML格式:

1
<jsp:directive.page attribute="value" />
属性 描述
buffer 指定out对象使用缓冲区的大小
autoFlush 控制out对象的 缓存区
contentType 指定当前JSP页面的MIME类型和字符编码
errorPage 指定当JSP页面发生异常时需要转向的错误处理页面
isErrorPage 指定当前页面是否可以作为另一个JSP页面的错误处理页面
extends 指定servlet从哪一个类继承
import 导入要使用的Java类
info 定义JSP页面的描述信息
isThreadSafe 指定对JSP页面的访问是否为线程安全
language 定义JSP页面所用的脚本语言,默认是Java
session 指定JSP页面是否使用session
isELIgnored 指定是否执行EL表达式
isScriptingEnabled 确定脚本元素能否被使用

(2)包含:<%@ include ... %>

使用包含操作,可以将一些重复的代码包含进来继续使用,从正常的页面组成来看,有时可能分为几个区域。而其中的一些区域可能是一直不需要改变的,改变的就其中的一个具体内容区域。现在有两种方法可以实现上述功能。

  • 方法一:在每个JSP 页面 (HTML)都包含工具栏、头部信息、尾部信息、具体内容
  • 方法二:将工具栏、头部信息、尾部信息都分成各个独立的文件,使用的时候直接导入

很明显,第二种方法比第一种更好,第一种会存在很多重复的代码,并旦修改很不方便,在JSP 中如果要想实现包含的操作,有两种做法:静态包含动态包含,静态包含使用 include 指令即可,动态包含则需要使用 include 动作标签。

  • 静态包含:<%@ include file="要包含文件的相对路径" %>

  • 动态包含:<jsp:include file="要包含文件的相对路径"> </jsp:include>

(3)Taglib 指令

JSP API允许用户自定义标签,一个自定义标签库就是自定义标签的集合。Taglib指令引入一个自定义标签集合的定义,包括库路径、自定义标签。

Taglib指令的语法:

1
<%@ taglib uri="uri" prefix="prefixOfTag" %>

2.3 MVC 模式

MVC 是一种分层开发的模式,其中:

  • M:Model,业务模型,处理业务
  • V:View,视图,界面展示
  • C:Controller,控制器,处理请求,调用模型和视图,也可以在一个控制器中调用另一个控制器
2022-05-04 11.29.09
三层架构:软件设计架构
2022-05-04 11.46.01
  • 数据访问层:对数据库的CRUD基本操作
  • 业务逻辑层:对业务逻辑进行封装,组合数据访问层层中基本功能,形成复杂的业务逻辑功能
  • 表现层:接收请求,封装数据,调用业务逻辑层,响应数据

2.4 案例

2022-05-04 12.59.01 2022-05-04 12.59.29 2022-05-04 12.59.46 2022-05-04 13.00.04

我们使用一个简单的例子实现上述 MVC + 三层结构,其中我们不需要连接数据库,所以我们把 “数据访问层”和“数据库”用一个类 Compte 取代:

Compte.java (DATA)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class Compte {
private int num;
private String nom;
private int solde;

public Compte() {} // 空构造函数

public Compte(int num, String nom, int solde) {
this.num = num;
this.nom = nom;
this.solde = solde;
}

public int getNum() {
return this.num;
}
public void setNum(int num) {
this.num = num;
}

public String getNom() {
return this.nom;
}
public void setNom(String nom) {
this.nom = nom;
}

public int getSolde() {
return this.solde;
}
public void setSolde(int solde) {
this.solde = solde;
}

public String toString() {
rerturn "Compte [num=" + num + ",nom=" + nom + ",solde=" + solde + "]";
}
}

Facade.java (Service)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Facade {
public Facade() {
addCompte(new Compte(1, "dan", 2000));
addCompte(new Compte(2, "alain", 4000));
addCompte(new Compte(3, "luc", 6000));
}

private Map<Integer, Compte> comptes = new Hashtable<Integer, Compte>();

public void addCompte(Compte c){
comptes.put(c.getNum(), c);
}

public Collection<Compte> consulterComptes() {
return comptes.values();
}

public Compte consulterCompte(int num) throws RuntimeException {
Compte c = comptes.get(num);
if (c == null)
throws new RuntimeException("Compte introuvable");
return c;
}

public void debit(int num, int montant) throws RuntimeException {
Compte c = consulterCompte(num);
if (c.getSolde() < montant)
throws new RuntimeException("Solde insuffisant");
c.setSolde(c.getSolde - montant);
}

public void credit(int num, int montant) throws RuntimeException {
Compte c = consulterCompte(num);
c.setSolde(c.getSolde + montant);
}

}

Controller.java (servlet)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@WebServlet("/Controller")
public class Controller extends HttpServlet {

private Facade facade = new Facade();

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try {
String action = request.getParameter("action");
if (action.equals("consulter")) {
int num = Integer.parseInt(request.getParameter("num"));
request.setAttribute("num", num);
request.setAttribute("compte", facade.consulterCompte(num));
}
else if (action.equals("consulterTous")){
request.setAttribute("comptes", facade.consulterComptes());
}
else if (action.equals("debit") || action.equals("credit")) {
int num = Integer.parseInt(request.getParameter("num"));
int montant = Integer.parseInt(request.getParameter("montant"));
request.setAttribute("num", num);
if (action.equals("debit")) {
facade.debit(num, montant);
} else {
facade.crebit(num, montant);
}
request.setAttribute("num", null);
}
} catch (Exception e) {
request.setAttribute("exception: ", e.getMessage());
}
request.getRequestDispatcher("Banque.jsp").forward(request, response);
}
}

Banque.jsp (View)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<%@ page language="java" import="java.util.*" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>

<body>
<form action="Controller" method="get">
<table>
<tr>
<td> Code :</td>
<td><input type="text" name="num" value="${num}"></td>
<!-- ${num} EL表达式 = request.getAttribute("num"); -->
<td><input type="submit" name="action" value="consulter"></td>
<td><input type="submit" name="action" value="consultertous"></td>
</tr>
</table>
</form>

<% if (request.getAttribute("compte") != null) { %>

<table>
<tr><td> Num :</td><td>${compte.num}</td></tr>
<tr><td> Nom :</td><td>${compte.nom}</td></tr>
<tr><td> Solde :</td><td>${compte.solde}</td></tr>
</table>

<form action="Controller" method="get">
<table>
<tr>
<td><input type="hidden" name="num" value="${num}"></td>
<td><input type="text" name="montant"></td>
<td><input type="submit" name="action" value="debit"></td>
<td><input type="submit" name="action" value="credit"></td>
</tr>
</table>
</form>

<% } %>
<% Collection<Compte> l = (Collection<Compte>)request.getAttribute("comptes");
if (l != null) {
%>

<table border="1" width="80%">
<tr> <th>Num</th><th>Nom</th><th>Solde</th></tr>
<% for (Compte c : l) { %>
<tr>
<td><%=c.getNum() %></td>
<td><%=c.getNom() %></td>
<td><%=c.getSolde() %></td>
</tr>
<% } %>
</table>
<% } %>

${exception}
</body>
</html>

第三部分:EJB

EBJ (Enterprise JavaBean) 运行在容器 (Container) 中。EJB 实际上就是一个封装了业务逻辑的 Java 类。

  • EJB 支持分布式。分布式对象之间实现分布透明性,即在客户端代码中无需指定分布式对象的位置;
  • EJB 支持分布式对象之间的事物(RMI 不支持)
  • 支持不同类型的客户端
2022-05-04 14.40.38

Session Bean

Session Bean 可以执行业务逻辑操作,比如注册用户、订单登记等。在上级部分所讲的“三层结构”中位于业务逻辑层

何为 Session?

从客户端获得 EJB 对象开始,多次调用 EJB 对象的方法,直到客户端生命周期结束,或客户端释放了 EJB 队象为止,称为一次会话 (Session)。与返回的 EJB 对象有关,如果不是同一个对象(内存地址不同),则不是同一个会话。

sequenceDiagram
    participant c as Client
    participant ec as EJB容器
    participant eo as EJB对象
    
    c ->> ec :1. JNDI 查找 EJB对象
    ec ->> c :2. 返回 EJB对象1
    c ->> eo :3. 调用 EJB对象1 的方法
    c ->> eo :4. 调用 EJB对象1 的方法
    c ->> eo :5. 调用 EJB对象1 的方法
    Note over eo :以上操作均在一个 Session1 中
    
    Note over c :客户端生命周期结束
    
    c ->> ec :6. JNDI 查找 EJB对象
    ec ->> c :7. 返回 EJB对象2
    c ->> eo :8. 调用 EJB对象2 的方法
    c ->> eo :9. 调用 EJB对象2 的方法
    c ->> eo :10. 调用 EJB对象2 的方法
    Note over eo :以上操作均在一个 Session2 中
Session Bean 的状态

何为对象的状态?

对象的状态是由其实例变量(即成员变量)的值组成。

  • 实例变量:在不同的实例中,变量的值可以是不同的。非静态变量。
  • 类变量:这个类的所有实例都一个值。static 变量
Stateful Session Bean (有状态的 Session Bean)

EJB 能够为同一个客户端在多次请求(方法调用)之间保持与其对应的状态信息

从 HTTP Session 类比 Stateful Session Bean:

客户动作 服务器响应
1、打开浏览器
2、访问购物网站 3、创建 HTTP Session 对象
4、返回 jsessionid
5、将 jsessionid 写入 cookie
6、往购物车内添加商品
7、向系统提交商品信息,以及 jsessionid 的值 8、服务器根据 jsessionid 找到对应的 HTTP Session对象,同时,创建购物车对象,与 Session 绑定
9、 继续添加商品,或删除商品
10、每次向服务器提交数据的时候,都会带着一个 jsessionid 的信息 11、服务器通过 jsessionid 来辨认不同的客户端,以及维护这些客户端的状态信息

EJB 实例池通过 Token 令牌区分不同的客户端,从而实现 Stateful Session Bean:

2022-05-04 15.01.19
Stateless Session Bean (无状态的 Session Bean)

是指 EJB 容器不会对 EJB 的状态进行管理。容器会使用实例池的方式,甚至单例 (Singleton) 的方式来实现无状态的 Session Bean。

是否可以理解成:在有状态的 Session Bean 中,实例池不再根据 Token 区分不同的客户端?

Singleton Session Bean (单例的 Session Bean)

实例池中只有一个 EJB对象,不再对客户端的状态进行区别管理,而统一使用一个 EJB 对象

客户端访问接口

@Remote 远程客户端接口
  • 客户端与其调用的 EJB 对象不在同一个 JVM 进程中;
  • 可以是 web组件、应用客户端、或是其他的 EJB;
  • 对于远程客户端来说,EJB 的位置是透明的;
  • 为了创建一个可以被远程客户端访问的 EJB,需要用 @Remote 注解来定义这些 EJB
@Local 本地客户端接口
  • 客户端与其调用的 EJB 对象在同一个 JVM 进程中;
  • 可以是 web组件、或是其他的 EJB;
  • 为了创建一个可以被本地访问的 EJB,需要用 @Local 注解来定义这些 EJB
  • 一个 EJB 可以同时被定义为 @Remote@Local
@webMethod 客户端接口

Web Service 客户端可以访问无状态 Session Bean 的接口,只有在业务逻辑方法被标识为 @WebMethod 的时候,Web Service 客户端才可以访问到。

客户端访问方式

远程访问方式 (Remote Access)

在进程间通行的时候需要将参数序列化和反序列化,传值。

sequenceDiagram
    participant c as Client
    participant est as EJB Stub 客户端代理
    participant esk as EJB Skeleton 服务器代理
    participant j as JNDI 服务
    participant eo as EJB对象
    
    c ->> +j :1. JNDI lookup(查找) EJB对象
    j ->> -est :2. 创建
    est ->> c :3. 给客户端返回 stub 对象
    c ->> +est :4. 调用方法(参数)
    est ->> est :5. 将参数序列化
    est ->> -esk :6. 底层网络通信
    esk ->> esk :7. 将参数反序列化
    esk ->> eo :8. 调用相应的方法(参数)
本地访问方式 (Local Access)

与远程访问方式相比,本地服务方式没有将参数序列化和反序列化的内容。可以直接访问地址(传址)

sequenceDiagram
    participant c as Client
    participant est as EJB Stub 客户端代理
    participant esk as EJB Skeleton 服务器代理
    participant j as JNDI 服务
    participant eo as EJB对象
    
    c ->> +j :1. JNDI lookup(查找) EJB对象
    j ->> -est :2. 创建
    est ->> c :3. 给客户端返回 stub 对象
    c ->> +est :4. 调用方法(参数)
    est ->> -esk :5. 底层网络通信
    esk ->> eo :6. 调用相应的方法(参数)
客户端访问接口类型与访问方式
graph LR
subgraph "客户端接口"
远程客户端接口
本地客户端接口
WebService客户端接口
end
subgraph "访问方式"
Remote方式
Local方式
WebMethod方式
end
远程客户端接口 --- Remote方式
本地客户端接口 --- Remote方式
本地客户端接口 --- Local方式
WebService客户端接口 --- WebMethod方式
方法的参数和访问方式

不同的访问方式 (Remote、Local、Web Service) 会影响到 EJB 方法的参数及其返回值。

独立性
  • 如果是远程调用,客户端操纵的 EJB 的参数,其实是一份参数值的拷贝。因此,对参数的修改,不会影响到EJB,
  • 但是对于本地调用来说,客户端操纵的 EJB 的参数,就是一个直接引用,它对参数的修改,将会影响到 EJB
  • 所以,不管在哪种情况下,请避免修改参数的值
粗粒度的数据访问

因为远程调用的速度比较慢,所以在设计的时候,请尽量使用粗粒度的接口设计。即尽量减少方法的调用,并尽可能在一次方法调用中传输完毕所需要的数据!

案例

我们依旧沿用上一个部分的案例。

Compte.java ( 实体类 )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

public class Compte extends Serializable { // 需要将其序列化
private int num;
private String nom;
private int solde;

public Compte() {} // 空构造函数

public Compte(int num, String nom, int solde) {
this.num = num;
this.nom = nom;
this.solde = solde;
}

public int getNum() {
return this.num;
}
public void setNum(int num) {
this.num = num;
}

public String getNom() {
return this.nom;
}
public void setNom(String nom) {
this.nom = nom;
}

public int getSolde() {
return this.solde;
}
public void setSolde(int solde) {
this.solde = solde;
}

public String toString() {
rerturn "Compte [num=" + num + ",nom=" + nom + ",solde=" + solde + "]";
}
}
@Remote 远程客户端访问 和 @Local 本地客户端访问

<interface>: BankLocal (本地访问接口)

1
2
3
4
5
6
7
8
@Local
public interface BankLocal {
public void addCompte(Compte c);
public Collection<Compte> consulterComptes();
public Compte consulterCompte(int num) throws RuntimeException;
public void debit(int num, int montant) throws RuntimeException;
public void credit(int num, int montant) throws RuntimeException ;
}

<interface>: BankRemote (远程访问接口)

1
2
3
4
5
6
7
8
@Remote
public interface BankRemote {
public void addCompte(Compte c);
public Collection<Compte> consulterComptes();
public Compte consulterCompte(int num) throws RuntimeException;
public void debit(int num, int montant) throws RuntimeException;
public void credit(int num, int montant) throws RuntimeException ;
}

BankImpl.java ( Facade/Service)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Singleton // Or @Stateful, or @Stateless
public class Bankimpl implements BankLocal, BankRemote {
@PostConstruct
public void init() {
addCompte(new Compte(1, "dan", 2000));
addCompte(new Compte(2, "alain", 4000));
addCompte(new Compte(3, "luc", 6000));
}

private Map<Integer, Compte> comptes = new Hashtable<Integer, Compte>();

public void addCompte(Compte c){
comptes.put(c.getNum(), c);
}

public Collection<Compte> consulterComptes() {
return comptes.values();
}

public Compte consulterCompte(int num) throws RuntimeException {
Compte c = comptes.get(num);
if (c == null)
throws new RuntimeException("Compte introuvable");
return c;
}

public void debit(int num, int montant) throws RuntimeException {
Compte c = consulterCompte(num);
if (c.getSolde() < montant)
throws new RuntimeException("Solde insuffisant");
c.setSolde(c.getSolde - montant);
}

public void credit(int num, int montant) throws RuntimeException {
Compte c = consulterCompte(num);
c.setSolde(c.getSolde + montant);
}

}

RemoteClientEJB.java (远程客户端app)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class RemoteClientEJB {
public static void main(String[] args) {
try {
String appName = "";
String moduleName = "jbbk"; // project name
String distinctName = "BankImpl";
String viewClassName = BankRemote.class.getName();
Context ctx = new InitialContext();
String jndiName = "ejb:" + appName + "/" + mouduleName + "/" + distinctName + "!"
+ viewClassName;
System.out.println(jndiName); // ejb:/jbbk/BankImpl!bk.BankRemote

// 远程获取 BankRemote 的代理
BankRemote bankStub = (BankRemote) ctx.lookup(jndiName);

Collection<Compte> comptes = bankStub.consulterComptes();
System.out.println("Print all the accounts(comptes).");
for (Compte c:comptes) {
System.out.println(c.toString);
}
} catch (Execption e) {
e.printStackTrace();
}
}
}

jboss-ejb-client.properties ( 为了客户端可以远程访问,配置 JNDI )

1
2
3
4
5
endpoint.name = client-endpoint
remote.connectionprovider.create.org.xnio.Options.SSL_ENABLED = false
remote.connections = default
remote.connections.default.host = localhost
remote.connections.default.port = 8080
@WebMethod 客户端访问:Servlet

Controller.java (以 @WebServlet 方式访问的客户端:Servlet)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@WebServlet("/Controller")
public class Controller extends HttpServlet {
private static final long serialVersionUID = 1L; // *
@EJB // * 服务器端的实现
private BankImpl facade = new Facade();

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try {
String action = request.getParameter("action");
if (action.equals("consulter")) {
int num = Integer.parseInt(request.getParameter("num"));
request.setAttribute("num", num);
request.setAttribute("compte", facade.consulterCompte(num));
}
else if (action.equals("consulterTous")){
request.setAttribute("comptes", facade.consulterComptes());
}
else if (action.equals("debit") || action.equals("credit")) {
int num = Integer.parseInt(request.getParameter("num"));
int montant = Integer.parseInt(request.getParameter("montant"));
request.setAttribute("num", num);
if (action.equals("debit")) {
facade.debit(num, montant);
} else {
facade.crebit(num, montant);
}
request.setAttribute("num", null);
}
} catch (Exception e) {
request.setAttribute("exception: ", e.getMessage());
}
request.getRequestDispatcher("Banque.jsp").forward(request, response);
}
}

<interface>: BankRemote (远程访问接口)

1
2
3
4
5
6
7
8
9
10
11
@Remote
@WebService
public interface BankRemote {
public void addCompte(Compte c);
public Collection<Compte> consulterComptes();
public Compte consulterCompte(int num) throws RuntimeException;
@WebMethod
public void debit(int num, int montant) throws RuntimeException;
@WebMethod
public void credit(int num, int montant) throws RuntimeException ;
}

BankImpl.java ( Facade/Service)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Singleton // Or @Stateful, or @Stateless
@WebService(endpointInterface="bk.BankRemote", serviceName="BankWS")
// endpointInterface = "name_of_interface.class"
// serviceName = "name_of_service_wanted"
public class Bankimpl implements BankLocal, BankRemote {
@PostConstruct
public void init() {
addCompte(new Compte(1, "dan", 2000));
addCompte(new Compte(2, "alain", 4000));
addCompte(new Compte(3, "luc", 6000));
}

private Map<Integer, Compte> comptes = new Hashtable<Integer, Compte>();

public void addCompte(Compte c){
comptes.put(c.getNum(), c);
}

public Collection<Compte> consulterComptes() {
return comptes.values();
}

public Compte consulterCompte(int num) throws RuntimeException {
Compte c = comptes.get(num);
if (c == null)
throws new RuntimeException("Compte introuvable");
return c;
}

public void debit(int num, int montant) throws RuntimeException {
Compte c = consulterCompte(num);
if (c.getSolde() < montant)
throws new RuntimeException("Solde insuffisant");
c.setSolde(c.getSolde - montant);
}

public void credit(int num, int montant) throws RuntimeException {
Compte c = consulterCompte(num);
c.setSolde(c.getSolde + montant);
}

}

第四部分:JPA

JPA,Java持久化API:持久化 Bean 与普通的 Java Bean 无异,区别在于他们要用 EJB 的 Annotation 进行标记。

  • 一个实体类将其表示为 @Entity
  • 实体类必须有主键,一般用 @Id 标识;
  • /META-INF/ 目录下,有 persistence.xml 文件,其主要作用是定义实体类映射的相关配置信息,比如指定数据源、都有哪些实体类、以及跟持久化相关的其它的一些属性。

Entity Bean 实体类

我们可以用 Entity Bean 实体类来记录数据,每个实体类都关联一个数据库的表

Entity Bean 基本映射规则(映射到数据库表)

(默认的表名和字段名与属性名一致)

  1. 实体类和表的映射关系:

    • @Entity:声明实体类
    • @Table(name="数据库表的名称"):配置实体类和表的映射关系
  2. 实体类中属性和数据库表中字段的映射关系:

    • @Id:声明主键的配置
    • @GeneratedValue(strategy):配置主键的生成策略
      • strategy = GenerationType.IDENTITY:id主键自增
      • strategy = GenerationType.AUTO:缺省,默认方式
      • strategy = GenerationType.SEQUENCE:通过序列生产主键,与 @SequenceGenerator 配合使用
      • strategy = GenerationType.TABLE:通过其他表生成主键
    • @Column(name="数据库表中字段的名称"):配置属性和数据库表中字段的映射关系
      • name = "数据库表中字段的名称" ;
      • unique = true/false:是否唯一;
      • nullable = true/false:是否可空;
      • length = 字段长度
    • @Basic:默认缺省,所有配置全部设为默认
    • @Transient:该属性不需要映射为数据库表的字段
    • @Temporal(TemporalType):定义 Date 类型的精度
      • TemporalType.DATE年-月-日
      • TemporalType.TIME时:分:秒
      • TemporalType.TIMESTAMP年-月-日 时:分:秒
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    @Entity
    public class Compte extends Serializable { // 需要将其序列化
    @Id
    @GeneratetdValue(strategy=GenerationType.IDENTITY)
    private int num;
    private String nom;
    private int solde;

    public Compte() {} // 空构造函数

    public Compte(int num, String nom, int solde) {
    this.num = num;
    this.nom = nom;
    this.solde = solde;
    }

    public int getNum() {
    return this.num;
    }
    public void setNum(int num) {
    this.num = num;
    }

    public String getNom() {
    return this.nom;
    }
    public void setNom(String nom) {
    this.nom = nom;
    }

    public int getSolde() {
    return this.solde;
    }
    public void setSolde(int solde) {
    this.solde = solde;
    }

    public String toString() {
    rerturn "Compte [num=" + num + ",nom=" + nom + ",solde=" + solde + "]";
    }
    }

EntityManager 实体类管理器

在 JPA 规范中,EntityManager是真正对数据库操作的对象。

实体作为普通 Java 对象,只有在调用EntityManager 将其持久化后才会变成持久化对象。EntityManager对象在一组实体类与底层数据源之间进行 O/R 映射的管理。它可以用来管理和更新 Entity Bean,根椐主键查找 Entity Bean,还可以通过 JPQL 语句查询实体。

实体的状态:
  • 新建状态:新创建的对象,尚未拥有持久性主键
  • 持久化状态:已经拥有持久性主键并和持久化建立了上下文环境
  • 游离状态 :拥有持久化主键,但是没有与持久化建立上下文环境
  • 删除状态:拥有持久化主键,已经和持久化建立上下文环境,但是从数据库中删除
EntityManager 方法
1
2
@PersistenceContext
private EntityManager em;
  • find():根据id查找

    1
    2
    int num;	// 主键
    Compte c = em.find(Compte.class, num);
  • getReference():根据id查找,先返回实体对象的代理,在使用代理对象前关闭 EntityManager 会出现懒加载异常 LazyInitializationException

  • presist():保存 (INSERT),使对象由临时状态变为持久化状态。如果实体类有主键,在 presist()会抛出异常

    1
    2
    Compte c = new Compte(null, "tom", "200");	// 自动分配主键
    em.presist(c);
  • merge():更新 UPDATE or INSERT

    • 如传入的是一个临时对象(无主键id),会先创建一个新的对象(有主键),把临时对象的属性复制到新对象中,再对这个新对象 INSERT 数据库持久化操作。所以新对象有主键,而临时对象没有主键。

      1
      2
      3
      Compte c = new Compte(null, "theo", "1000");	// 自动分配主键
      Compte c_temp = em.merge(c);
      // c.getNum(); -> null c_temp.getNum(); -> JPA 按照策略分配的 #Id 的值
    • 如果传入的是一个游离对象(有主键id),若 EntityManager 缓存中没有该对象、且数据库中也没有相应的记录,JPA 则会先创建一个新的对象,把游离对象的属性复制到新对象中,再对这个新对象 INSERT 数据库持久化操作

      1
      2
      3
      Compte c = new Compte(100, "enzo", "1000");	// 自动分配主键
      Compte c_temp = em.merge(c);
      // c.getNum(); -> 100 c_temp.getNum(); -> 4
    • 如果传入的是一个游离对象(有主键id),若 EntityManager 缓存中没有该对象,但数据库中有相应的记录,JPA 则会查询相应记录返回该记录的一个对象,把游离对象的属性复制到查询对象中,再对这个查询对象 UPDATE 数据库持久化操作

      1
      2
      3
      4
      Compte c = new Compte(null, "enzo", "500");	// 自动分配主键
      c.setNum(4);
      Compte c_temp = em.merge(c);
      // c.toString(); -> Compte [num=4, nom=enzo, solde=500]
  • remove():删除

  • flush():同步数据表的记录和内存中对象的状态

    • setFlushMode(FlushModeType.AUTO):自动同步到数据库
    • setFlushMode(FlushModeType.COMMIT):直到事物提交时才同步到数据库
    • getFlushMode
  • transaction = em.getTransaction():获取事务对象

    • transaction.begin();
    • transaction.commit();
    • transaction.rollback();

映射关联关系

@ManyToOne 单向多对一关联映射

在单向多对一的关联映射里,在“多”的一端添加一个外键 (Foreign Key) 指向“一”的一端,而且可以指定字段名称。

1
2
3
4
5
6
7
8
9
10
11
// 一
@Entity
public class Client {
@GeneratedValue
@Id
private int id;

@Column(name="client_name")
private String name;
// constructors, setters, getters
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 多
@Entity
public class Account {
@GeneratedValue
@Id
private int id;

@Column(name="account_amount")
private int amount;

@ManyToOne // @ManyToOne 多对一映射关系
@JoinColumn(name="client_ID") // 使用 @JoinColumn 来映射外键,name为新建外键列的列名
private Client client; // 多个账户可以对应一个客户
}
2022-05-05 19.57.54
@OneToMany 一对多关联映射
  • @OneToMany(fetch=FetchType.xxx):修改默认加载策略,默认懒加载 (LAZY);(EAGER)
  • @OneToMany(cascade=CascadeType.remove)
    • 默认情况下,若删除“一”的一端,会先将“一”的一端中关联“多”的外键置空,然后删除
    • 可以通过 @OneToManycascade 属性来修改默认的删除策略
    • CascadeType.PERSIST (级联新建)
    • CascadeType.REMOVE (级联删除)
    • CascadeType.REFRESH (级联刷新)
    • CascadeType.MERGE (级联更新)中选择一个或多个
    • 还有一个选择是使用CascadeType.ALL ,表示选择全部四项
一对多单向关联映射:

一对多单向关联映射 有两个映射策略,即 外键关联表关联

  • 外键关联:会创建一个外键 ,用外键记录 ClientAccount 之间的单向关联
1
2
3
4
5
6
7
8
9
10
11
12
// 一
@Entity
public class Client {
@Id
private int id;
@Column(name="client_name")
private String name;

@JoinColumn(name="client_ID") // 使用 @JoinColumn 来映射外键,name为新建外键列的列名
@OneToMany // @OneToMany 一对多映射关系
private Set<Account> accounts; // 集合属性:一个客户可以对应多个账户
}
1
2
3
4
5
6
7
8
9
10
11
// 多
@Entity
public class Account {
@Id
@GenerateValue
private int id;
@Column(name="account_amount")
private int amount;

// private Client client; // 因为是单向的,所以不能 Account 类里不能有 Client 类
}
2022-05-05 19.57.54
  • 关联:会创建一个中间表 ,用中间表记录 ClientAccount 之间的单向关联
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 一
@Entity
public class Client {
@Id
private int id;
@Column(name="client_Name")
private String name;

@JoinTable(name="t_Client_Account",
joinColumn={@JoinColumn(name="client_ID",referencedColumnName="ID")},
inverseJoinColumn={@JoinColumn(name="account_ID",referencedColumnName="ID")}
@OneToMany // @OneToMany 一对多映射关系
private Set<Account> accounts; // 集合属性:一个客户可以对应多个账户
}
1
2
3
4
5
6
7
8
9
10
11
// 多
@Entity
public class Account {
@Id
@GenerateValue
private int id;
@Column(name="account_amount")
private int amount;

// private Client client; // 因为是单向的,所以不能 Account 类里不能有 Client 类
}
2022-05-05 20.44.11
一对多双向关联映射:

一对多双向关联映射 = 多对一双向关联映射

没有中间表,但会在“多”的一端加入一个外键,两端的外键名保持一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 一
@Entity
public class Client {
@Id
@GenerateValue
private int id;
@Column(name="client_name")
private String name;

// 在“一”的一端定义mappedBy,即映射规则由对方决定; 在使用这种属性时,则不能用@JoinColumn属性
@OneToMany(mappedBy=client)
private Set<Account> accounts;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// 多
@Entity
public class Account {
@Id
@GenerateValue
private int id;
@Column(name="account_amount")
private int amount;

@JoinColumn(name="client_ID") // 使用 @JoinColumn 来映射外键,两端的外键名保持一致
@ManyToOne
private Client client; //
}
2022-05-05 19.57.54
@OneToOne 一对一双向关联映射

有两种策略:主键(Primary Key)关联唯一外键(Foreign Key)关联

主键关联

一端的主键 (@Id) 依赖于另一端的主键 (@Id)

1
2
3
4
5
6
7
8
9
10
11
12
13
@Entity
public class Manager {
@Id
@GenerateValue
private int id;

@Column(name="manager_name")
private String managerName;

@OneToOne
@PrimaryKeyJoinColumn
private CustomerNum customerNum
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Entity
public class Department {
@Id
@GenerateValue
private int id;

@Column(name="department_name")
private String DepartmentName;

@OneToOne(mappedBy="m_ID")
// 在双键关联中只需要定义一方的映射规则,另外一方遵守已经定义一方的映射规则;
// mappedBy = 对方指向我自身的 *属性名称*
private Customer customer
}
2022-05-05 21.07.25
唯一外键关联
  • 使用 @OneToOne 来映射一对一关联关系;
  • 若需要在当前表中添加外键,则需要使用 @JoinColumn(unique=true) 来映射
1
2
3
4
5
6
7
8
9
10
11
12
13
@Entity
public class Manager {
@Id
@GenerateValue
private int id;

@Column(name="manager_name")
private String managerName;

// 对于没有外键的一方,建议设置 mappedBy=true
@OneToOne(mappedBy="manager")
private Department department
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Entity
public class Department {
@Id
@GenerateValue
private int id;

@Column(name="department_name")
private String DepartmentName;

@OneToOne
@JoinColumn(name="manager_ID", unique=true)
// 在双键关联中只需要定义一方的外键映射规则,另外一方遵守已经定义一方的映射规则;
// 需要使用 @JoinColumn(unique=true) 来添加外键映射
private Manager manager
}
2022-05-05 21.07.25
@ManyToMany 多对多关联映射

​ 通过新建一个中间表,达到多对多的关联映射。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Entity
public class Item {
@Id
@GenerateValue
private int id;

@Column(name="item_name")
private String name;

@JoinTable( // 映射中间表
name="t_Item_Category", // 中间表的名字
joinColumns={ // 映射当前类所在的表 在中间表中的外键
@JoinColumn(name="item_ID", // 当前表在中间表中的 外键列的列名
referencedColumnName="ID")}, // 指定外键关联当前表的哪一列(可缺省)
inverseJoinColumns={ // 映射关联的类 在 中间表 中的 外键
@JoinColumn(name="category_ID", // 对方表在中间表中 外键列的列名
referenceColumnName="ID")}) // 指定外键关联对方表的哪一列(可缺省)
@ManyToMany
private Set<Category> categorys
}
1
2
3
4
5
6
7
8
9
10
11
12
@Entity
public class Category {
@GeneratedValue
@Id
private int id;

@Column(name="category_name")
private String name;

@ManyToMany(mappedBy="categorys")
private Set<Item> items;
}
2022-05-05 21.22.35

JPQL

JPQL 语言,即 Java Persistence Query Language 的简称。

JPQL 是一种和 SQL 非常类似的中间性和对象化查询语言,它查询的内容是实体类和类中的属性。它最终会被编译成针对不同底层数据库的 SQL查询,从而屏蔽不同数据库的差异。

JPQL 语言的语句可以是 select 语句,update 语句或 delete语句,它们都通过 Query 接口封装执行。调用 EntityManagercreateQuery()createNamedQuery()createNativeQuery() 以获得查询对象,进而可调用 Query 接口的相关方法来执行查询操作

例:

1
2
3
4
5
6
7
8
public Collection<Orders> searchByAge() {
String jpql = "FROM Customer c WHERE c.age > ?"; // ?是参数的占位符
Query query = em.createQuery(jpql);
// 占位符索引从1开始
// query.setParameter(第几个占位符, 参数的值)
query.setParameter(1,1);
Collection<Customer> result = (Collection<Customer>) query.getResultList();
}
  • SELECT-FROM 子句:select 用于指定返回的结果实体或实体的某些属性;from 子句声明查询源实体类,并指定标识符变量(相当于 SQL 表的别名)。如果不希望返回重复实体,可以使用关键字 distinct

    • FROM Orders WHERE o.id=:o_id 当查询整个实体类时可省略 select *
  • WHERE 子句:用于指定查询条件,

    • SELECT o FROM Orders o WHERE o.id =:o_id
    • SELECT o FROM Orders o WHERE o.id > 4
  • GROUP BY 子句:用于对查询结果分组统计,通常需要使用聚合函数,如 AVG, SUM, COUNT, MAX, MIN

    • SELECT MAX(o.id) FROM Orders o
  • ORDER BY 子句:用于对查询结果进行排序,ASC 升序,DESC 降序

    • SELECT o FROM Orders ORDER BY o.id DESC
  • UPDATE 子句:用于执行数据的更新操作。主要针对于单个实体类的更新

    1
    2
    3
    4
    5
    String jpql = "UPDATE Order o SET o.age = ? WHERE o.id = ?";
    Query query = em.creatQuery(jpql);
    query.setParameter(1,25);
    query.setParameter(2,2);
    query.executeUpdate();
  • DELETE 子句:

    1
    2
    3
    4
    String jpql = "DELETE Order o WHERE o.id = ?";
    Query query = em.creatQuery(jpql);
    query.setParameter(1,2);
    query.executeUpdate();

当然还有很多其他的用法,但在此我们不作深究。