JavaWeb基礎知識總結.
1.web服務器與HTTP協議
Web服務器
l WEB,在英語中web即表示網頁的意思,它用於表示Internet主機上供外界訪問的資源。
l Internet上供外界訪問的Web資源分為:
• 靜態web資源(如html 頁面):指web頁面中供人們瀏覽的數據始終是不變。
• 動態web資源:指web頁面中供人們瀏覽的數據是由程序產生的,不同時間點訪問web頁面看到的內容各不相同。
l 靜態web資源開發技術
• Html
l 常用動態web資源開發技術:
• JSP/Servlet、ASP、PHP等 ruby python
• 在Java中,動態web資源開發技術統稱為Javaweb,我們課程的重點也是教大家如何使用Java技術開發動態的web資源,即動態web頁面。
但是我們做java開發,不是做網頁。
網絡上的資源分為兩種
早期:靜態頁面 html實現。 觀看
現在:動態頁面 php asp jsp 交互.
lamp =linux +apache+ mysql+php----->個人網關或小型企業首選
asp現在沒人用,但是網絡上遺留下來的比較多。miscrosoft的技術
.net技術。
jsp--->java去做網頁所使用的技術。jsp本質上就是servlet
使用jsp開發成本高。
BS====>瀏覽器+服務器 只要有瀏覽器就可以
CS----->客戶端+服務器. 必須的在客戶端安裝程序.
現在基本上開發的都是BS程序
BS怎樣通信:
必須有請求有響應。
有一次請求就應該具有一次響應,它們是成對出現的。
服務器介紹
大型服務器:websphere(IBM),weblogic(Oracle) J2EE容器 -
支持EJB (EnterPrice Java Bean (企業級的javabean)) – Spring
weblogic BEA公司產品,被Oracle收購,全面支持JavaEE規范,收費軟件,企業中非常主流的服務器 -------- 網絡上文檔非常全面
WebSphere 文檔非常少,IBM公司產品,價格昂貴,全面支持JavaEE 規范
Tomcat- apache,開源的。Servlet容器。
tomcat 開源小型web服務器 ,完全免費,主要用於中小型web項目,只支持Servlet和JSP 等少量javaee規范 ,Apache公司jakarta 一個子項目
Jboss – hibernate公司開發。不是開源免費。J2EE容器
Tomcat安裝
注意路徑中不要包含空格與中文。
Ø 安裝步驟
1、tomcat.apache.org 下載tomcat安裝程序
Tomcat7安裝程序 ---- zip免安裝版
2、解壓tomcat
3、配置環境變量 JAVA_HOME 指向JDK安裝目錄 D:\Program Files\Java\jdk1.6.0_21
*CATALINA_HOME指定tomcat安裝目錄
4、雙擊tomcat/bin/startup.bat
5、在瀏覽器中 輸入 localhost:8080 訪問tomcat主頁了
Ø 注意問題:
啟動黑色不能關閉
1、CATALINA_HOME 指定tomcat安裝位置 --- 可以不配置
2、JAVA_HOME 指定JDK安裝目錄,不要配置bin目錄,不要在結尾加;
3、端口被占用
啟動cmd
netstat -ano 查看占用端口進程pid
任務管理器 查看---選擇列 顯示pid -- 根據pid結束進程
* 有些進程無法關系(系統服務 --- 必須結束服務) win7 自帶 World wide web publish IIS服務 默認占用端口80
* xp 安裝apache服務器后,會占用80 端口 ,關閉apache服務
通過運行 services.msc 打開服務窗口 關閉相應服務
tomcatc目錄結構
-----bin 它里面裝入的是可執行的命令 如 startup.bat
-----conf 它里面是一個相關的配置文件,我們可以在里面進行例如端口,用戶信息的配置
<Connector port="80" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
-----lib tomcat類庫。
-----logs tomcat 日志文件
-----temp 臨時文件
-----webapps 它里面放的是的 web site(web項目)
-----work 存放的是頁面(例如 jsp)轉換成的.class文件。
2.創建網站,將網站發布到tomcat服務器上
創建網站根目錄
在根目錄下 創建靜態web資源和動態web資源
Web site
---- *.html *.css *.js 圖片 音頻 視頻 、*.jsp
---- WEB-INF目錄 存放java程序和配置文件
--- classes 存放.class文件
--- lib 存放.jar 文件
--- web.xml 網站核心配置文件
*** 如果靜態網站可以不存在 WEB-INF目錄的 ,WEB-INF目錄,客戶端無法直接訪問(在服務器內存通過程序訪問)
將網站發布到tomcat -----------虛擬目錄映射
虛似目錄的映射方式有三種
1.在開發中應用的比較多 直接在webapps下創建一個自己的web site就可以.
步驟 1.在webapps下創建一個myweb目錄
2.在myweb下創建WEB-INF目錄,在這個目錄下創建web.xml
3.將web.xml文件中的xml聲明與根元素聲明在其它的web site中copy過來。
4.在myweb下創建一個index.html文件
5.啟動tomcat
6.在瀏覽器中輸入 http://localhost/myweb/index.html
以下兩種方式,可以將web site不放置在tomcat/webapps下,可以任意放置
2.在server.xml文件中進行配置
<Context path="/abc" docBase="C:\myweb1"/>
</Host>
在Host結束前配置
path:它是一個虛擬路徑,是我們在瀏覽器中輸入的路徑
docBase:它是我們web sit的真實路徑
http://localhost/abc/index.html
3.不在server.xml文件中配置
而是直接創建一個abc.xml文件
在這個xml文件中寫
<Context path="" docBase="C:\myweb1"/>
將這個文件放入conf\Catalina\localhost
http://localhost/abc/index.html
3.生成war文件
war文件是web項目的壓縮文件。
要想生成,先將要壓縮的內容壓縮成zip文件,
然后將后綴改成war就可以,
war文件可以直接在服務器上訪問。
關於tomcat-manager
可以在conf/tomcat-users.xml中進行用戶信息添加
<role rolename="manager"/>
<user username="xxx" password="xx" roles="manager"/>
這樣就添加了一個用戶
注意,用戶權限要是比較大的話,會出現安全問題.
4.虛擬主機
做自己的一個http://www.baidu.com
1.訪問一個網站的過程
http://www.baidu.com
http 協議
www 服務器
.baidu.com 域名 IP
步驟
1.上網將baidu首頁下載下來
2.做一個自己的web site 首頁就是下載下來的頁面。
別忘記創建WEB-INF在它下面的web.xml文件中
<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
這句話的作用是默認訪問頁面是index.html
3.在tomcat中的conf文件夾下的server.xml中修改
<Host name="www.baidu.com" appBase="c:\baidu"
unpackWARs="true" autoDeploy="true"
xmlValidation="false" xmlNamespaceAware="false">
<Context path="" docBase="c:\baidu"/>
</Host>
4.在windows/system32/drivers/etc/hosts中添加
127.0.0.1 www.baidu.com
目的是當訪問www.baidu.com時其實訪問的是本機。
5.打開瀏覽器在地址欄中輸入www.baidu.com
這時其時訪問的是我們自己
web site中的頁面。
5.使用myeclipse創建web project與tomcat集成
我們在myeclipse中創建web project有一個WebRoot目錄。
但是我們發布到tomcat中沒有這個,它其時就是我們工程的名稱.
步驟
1.創建web工程
2.在eclipse中配置tomcat服務器
window/屬性/myeclipse/service中配置自己的tomcat目錄.
注意到tomcat根目錄就可以了。不要到bin中。
如果不好使用,看一些jdk是否配置.
1. 將webproject部署到tomcat中
6.HTTP協議
HTTP是hypertext transfer protocol(超文本傳輸協議)的簡寫,它是TCP/IP協議的一個應用層協議,用於定義WEB瀏覽器與WEB服務器之間交換數據的過程。
HTTP協議是學習JavaWEB開發的基石,不深入了解HTTP協議,就不能說掌握了WEB開發,更無法管理和維護一些復雜的WEB站點。
示例1
telnet怎樣使用
1.telnet localhost 8080
2 ctrl+]
3.按回車
注意 在里面寫錯的內容不能修改
GET /index.html HTTP/1.1
host:localhost
4.要敲兩次回車
HTTP/1.0版本只能保持一次會話
HTTP/1.1版本可能保持多次會話.
是根據telnet得到的響應信息
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
ETag: W/"7347-1184876416000"
Last-Modified: Thu, 19 Jul 2007 20:20:16 GMT
Content-Type: text/html
Content-Length: 7347
Date: Thu, 25 Apr 2013 08:06:53 GMT
Connection: close
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>Apache Tomcat</title>
<style type="text/css">
..........
示例2
是根據httpwatch得到的請求信息與響應信息
請求
GET / HTTP/1.1
Accept: application/x-shockwave-flash, image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*
Accept-Language: zh-cn
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)
Accept-Encoding: gzip, deflate
Host: localhost
Connection: Keep-Alive
響應
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
ETag: W/"7347-1184876416000"
Last-Modified: Thu, 19 Jul 2007 20:20:16 GMT
Content-Type: text/html
Content-Length: 7347
Date: Thu, 25 Apr 2013 08:12:57 GMT
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang
請求信息詳解
GET /books/java.html HTTP/1.1 ---------->請求行
Get是請求方式 /books/java.html 請求資源 HTTp/1.1協議版本
POST與GET的區別
1.什么樣是GET 請求 1)直接在地址欄輸入 2.超連接 <a></a> 3.form表單中method=get
什么樣是POSt請求 form表單中method=POST
2.以get方式提交請求時,在請求行中會將提交信息直接帶過去
格式 /day03_1/login?username=tom&password=123
以post方式提交時,信息會在正文中。
POST /day03_1/login HTTP/1.1
Accept: application/x-shockwave-flash, image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*
Referer: http://localhost/day03_1/login.html
Accept-Language: zh-cn
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate
Host: localhost
Content-Length: 25
Connection: Keep-Alive
Cache-Control: no-cache
username=tom&password=123
3. get方式最多能提交1kb
post可以提交大數據,做上傳時必須是post
Accept: */* 允許訪問mime類型,類型都在tomcat 的conf/web.xml文件中定義了。
這個需要知道,因為做下載時要知道mime類型
Accept-Language: en-us 客戶端的語言
Connection: Keep-Alive 持續連接
Host: localhost 客戶端訪問資源
Referer: http://localhost/links.asp (重點) 防盜鏈。
User-Agent: Mozilla/4.0 得到瀏覽器版本 避免兼容問題
Accept-Charset: ISO-8859-1 客戶端字符編碼集
Accept-Encoding: gzip, deflate gzip是壓縮編碼.
If-Modified-Since: Tue, 11 Jul 2000 18:23:51 GMT 與Last-MOdified一起可以控制緩存。
Date: Tue, 11 Jul 2000 18:23:51 GMT
示例1
防盜鏈程序
referer.htm頁面
<body>
<a href="referer">referer</a>
</body>
RefererServlet類似
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String msg = request.getHeader("Referer");
if (msg != null && "http://localhost/day03_1/referer.html".equals(msg)) {
// 如果你是正常訪問,我們給其一個友好信息
response.getWriter().write("hello");
} else {
// 如果是盜鏈過來的,對不。罵它一句
response.getWriter().write("fuck...");
}
}
怎樣破解
URL url = new URL("http://localhost/day03_1/referer"); //得到一個url
URLConnection con = url.openConnection(); //訪問這個url,並獲得連接對象
con.addRequestProperty("Referer",
"http://localhost/day03_1/referer.html");
InputStream is = con.getInputStream(); // 讀取服務器返回的信息.
byte[] b = new byte[1024];
int len = is.read(b);
System.out.println(new String(b, 0, len));
http協議響應
HTTP/1.1 200 OK 響應狀態行
HTTP/1.1 200 OK
1xx 什么都沒做直接返回
2xx 成功返回
3xx 做了一些事情,沒有全部完成。
4xx 客戶端錯誤
5xx 服務器錯誤
200 正確
302 重定向
304 頁面沒有改變
404 未找到頁面
500 服務器出錯.
Location: http://www.it315.org/index.jsp 響應路徑(重點)+302
Server:apache tomcat
Content-Encoding: gzip 響應編碼 gzip 壓縮
Content-Length: 80 響應長度
Content-Language: zh-cn 響應語言
Content-Type: text/html; charset=GB2312 響應字符編碼
Last-Modified: Tue, 11 Jul 2000 18:23:51 GMT 要與請求中的 If-Modified-Since處理緩存
Refresh: 1;url=http://www.it315.org 自動跳轉
Content-Disposition: attachment; filename=aaa.zip (重要) 文件的下載
//下面三個是禁用瀏覽緩存
Expires: -1
Cache-Control: no-cache
Pragma: no-cache
Connection: close/Keep-Alive
Date: Tue, 11 Jul 2000 18:23:51 GMT
重點
今天可以講
Location: http://www.it315.org/index.jsp 響應路徑(重點)+302
Last-Modified: Tue, 11 Jul 2000 18:23:51 GMT 要與請求中的 If-Modified-Since處理緩存
Refresh: 1;url=http://www.it315.org 自動跳轉
我們在得到響應信息,經常得到的是壓縮后的。
這種操作
1.服務器配置方式
tomcat配置實現壓縮
80端口沒有配置 00:00:00.000 0.228 7553 GET 200 text/html http://localhost/
8080端口配置 00:00:00.000 0.027 2715 GET 200 text/html http://localhost:8080/
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" compressableMimeType="text/html,text/xml,text/plain" compression="on"/>
2.通過我們編程實現.(后面會講)
后面會講
Content-Disposition: attachment; filename=aaa.zip (重要) 文件的下載
//下面三個是禁用瀏覽緩存
Expires: -1
Cache-Control: no-cache
Pragma: no-cache
4.啟動服務器
5.在瀏覽器中訪問web資源.
Servlet
什么是Servlet
l Servlet是一個功能,如果你希望你的項目功能多一些,那就要多寫一此Servlet;
l Servlet是JavaWeb三大組件之一,也是最重要的組件!
Ø 三大組件:Servlet、Filter、Listener
l Servlet是一個我們自定義的Java類,它必須要實現javax.servlet.Servlet接口。
l Servlet是動態資源!
l Servlet必須在web.xml中進行配置后,才能被訪問。(把Servlet與一個或多個路徑綁定在一起)
如何實現Servlet
l 實現Servlet有三種方式:
Ø 實現Servlet接口;
Ø 繼承GenericServlet類;
Ø 繼承HttpServlet類(最佳選擇)。
3 Servlet的helloworld項目
3.1 手動完成
看代碼
3.2 MyEclipse完成
看代碼
4 Servlet的生命周期
4.1 生命周期相關方法
l Servlet接口中一共是5個方法,其中有三個是生命周期方法。
Ø void init(ServletConfig):這個方法會在Servlet被創建后,馬上被調用。只會被調用一次!我們可以把一些初始化工作放到這個方法中,如果沒有什么初始化工作要做,那么這個方法就空着就可以了。
² Servlet有兩個時間點會被創建:一是在第一次被請求時,會被創建;二是Tomcat啟動時被創建,默認是第一種,如果希望在tomcat啟動時創建,這需要在web.xml中配置。
Ø void destroy():這個方法會在Servlet被銷毀之前被調用。如果你有一些需要釋放的資源,可以在這個方法中完成,如果沒有那么就讓這個方法空着。這個方法也只會被調用一次!
² Servlet輕易不會被銷毀,通常會在Tomcat關閉時會被銷毀。
Ø void service(ServletRequest,ServletResponse):它會在每次被請求時調用!這個方法會被調用0~N次。
Ø String getServletInfo():它不是生命周期方法,也就是說它不會被tomcat調用。它可以由我們自己來調用,但我們一般也不調用它!你可以返回一個對當前Servlet的說明性字符串。
Ø ServletConfig getServletConfig():這個方法返回的是ServletConfig,這個類型與init()方法的參數類型相同。它對應的是web.xml中的配置信息,即<servlet>
4.2 ServletConfig、ServletContext、ServletRequest、ServletResponse
l ServletRequest:封裝了請求信息;
l ServletResposne:用於向客戶端響應;
l ServletContext:它可以在多個Servlet中共享數據。
l ServletConfig:它與<servlet>對應!
Ø 在<servlet>中可以配置<init-param>,即初始化參數,可以使用ServletConfig的getInitParameter(String),方法的參數是初始化參數名,方法的返回值是初始化參數值。
Ø getInitParameterNames(),該方法返回一個Enumeration對象,即返回所有初始化參數的名稱。
Ø String getServletName(),它返回的是<servlet-name>元素的值
Ø ServletContext getServletContext(),它可以獲取Servlet上下文對象。
5 GenericServlet
l 它代理了ServletConfig的所有功能。所有使用ServletConfig才能調用的方法,都可以使用GenericServlet的同名方法來完成!
l 不能覆蓋父類的init(ServltConfig)方法,因為在父類中該方法內完成了this.config=config,其他的所有ServletConfig的代理方法都使用this.config來完成的。一旦覆蓋,那么this.config就是null。
l 如果我們需要做初始化工作,那么可以去覆蓋GenericServlet提供的init()方法。
6 HttpServlet
l 它提供了與http協議相關的一些功能。
l 只需要去覆蓋doGet()或doPost()即可。這兩個方法,如果沒有覆蓋,默認是響應405!
7 Servlet細節
7.1 Servlet單例、線程不案例
l Servlet是的單例的。所以一個Servlet對象可能同時處理多個請求;
l Servlet不是線程安全的。
Ø 盡可能不創建成員變量,因為成員變量多個線程會共享!
Ø 如果非要創建,那么創建功能性的,只讀!
7.2 Servlet的創建時間:第一次被請求、啟動創建
* Servlet可以在第一次請求時被創建,還可以在容器啟動時被創建。默認是第一次請求時!
* 在<servlet>添加一個<load-on-startup>大於等於0的整數</load-on-startup>
* 如果有多個Servlet在容器啟動時創建,那么<load-on-startup>的值就有用了,創建的順序使用它的值來排序!
7.3 <url-pattern>的配置
l <url-pattern>中可以使用“*”表示所有字符,但它不匹配“/”。它的使用要求:
Ø 它要么在頭,要么在尾。不能在中間;
Ø 如果不使用通配符,那么必須使用“/”開頭。
l 如果一個訪問路徑,匹配了多個<url-pattern>,那么誰更加明確就匹配誰。
7.4 web.xml的繼承
l 每個項目都有一個web.xml,但tomcat下也有一個web.xml,在${CATALINA_HOME}\conf\web.xml
l conf\web.xml是所有項目的web.xml父文件,父文件中的內容等於同寫在子文件中。
ServletContext
Servlet三大域對象:
l ServletContext:范圍最大,應用范圍!
l HttpSession :會話范圍!
l HttpServletRequest:請求范圍!
域對象之一
域對象都有存取功能:
setAttribute(“attrName”, attrValue );//put
Object attrValue = getAttribute(“attrName”);//get
removeAttribute(“attrName”);//remove
1 ServletContext的作用
l 存取域屬性,ServletContext是一個域對象;
l 可以用來獲取應用初始化參數;
l 獲取資源
ServletContext的生命周期
l ServletContext在容器啟動時就被創建了;
l ServletContext在容器關閉時才會死!
l 一個項目只有一個ServletContext對象。
3 獲取ServletContext
l 通過ServletConfig的getServletContext()方法來獲取!
Ø ServletConfig是init()方法的參數,那只能在init()方法中獲取了;
Ø GenericServlet代理了ServletConfig的所有方法,而且還提供了getServletConfig(),所以在GenericServlet的子類中可以使用如下方式來獲取ServletContext對象:
² this.getServletContext()
² this.getServletConfig().getServletContext()
Ø HttpSession也有這個方法,session.getServletContext()。
4 域對象:ServletContext
l void setAttribute(String name, Object value):存儲屬性;
l Object getAttribute(String name):獲取屬性;
l void removeAttribute(String name):移除屬性;
l Enumeration getAttributeNames():獲取所有屬性名稱;
5 獲取初始化參數
一個 項目不只是可以配置servlet的初始化參數,還可以配置應用初始化參數
下面就是在web.xml中配置應用的初始化參數,這些參數需要使用ServletContext來獲取
<context-param> <param-name>p1</param-name> <param-value>v1</param-value> </context-param> <context-param> <param-name>p2</param-name> <param-value>v2</param-value> </context-param> |
l String getInitParameter(String name):通過參數名獲取參數值;
l Enumeration getInitParameterNames():獲取所有參數的名稱;
6 獲取資源
l 獲取真實路徑:getRealPath(String path):路徑必須以“/”開頭!它相對當前項目所在路徑的。
l 獲取指定路徑下的所有資源路徑:Set set = sc.getResourcePaths(“/xxx”)
l 獲取資源流:InputStream in = sc.getResourceAsStream(“/xxx”)
7 Class和ClassLoader獲取資源
User.class如何變成Class<User>的呢,由ClassLoader完成的!把硬盤上的User.class加載到內存,變成Class對象。
使用它們獲取資源流!它們相對類路徑(classpath)
request&response 對象
response
1. response簡介
l response的類型為HttpServletResponse,它是Servlet的service()方法的參數。
l 當客戶端發出請求時,tomcat會創建request和rsponse來調用Servlet的service()方法,每次請求都會創建新的request和response。
l response是用來向客戶端完成響應。
2 response的兩個流,用來響應正文
l response.getWriter() ,返回值為PrintWriter,用響應字符數據。
l response.getOutputStream(),返回值為ServletOutputStream,用來響應字節數據。
l 在一個請求范圍內,這兩個流不能同時使用!不然會輸出非法狀態異常。
3 response字符流的編碼問題
l response的字符流默認使用ISO-8859-1編碼,可以使用response.setCharaceterEncoding(“utf-8”)來設置編碼;
l 瀏覽器在沒有得到Content-Type頭時,會使用GBK來解讀字符串,當如果你設置了Content-Type,會使用你指定編碼來解讀字符串。response.setContentType(“html/texgt;charset=utf-8”);
4 response字符流的緩沖區
l response字符流緩沖區大小為8KB;
l 可以調用response.getWriter().flush()方法完成刷新,這會把當前緩沖區中的數據發送給客戶端。
l 當response一旦開始了發送,那么response的內部會有一個提交狀態為true。可以調用response的isCommitted()方法來查看當前的提交狀態。
5 自動刷新
l 有一個響應頭:Refresh,它的作用是在指定的時間后,自動重定向到指定路徑。例如:response.setHeader(“Refresh”, “5;URL=http://www.baidu.com”);,表示在5秒后自動跳轉到百度。
6 設置狀態碼
l response.sendError(404, “沒找到您訪問的資源”)
l response.sendStatus(302);
7 重定向
l 重定向:兩個請求。
Ø 第一個請求,服務器響應碼:302
Ø 第一個請求的響應頭有一個Location頭,它說明了要重定向的URL;
Ø 第二個請求,瀏覽器重新向Location頭指定的URL發出。
l 重定向:可以重定向到本項目之外的頁面。例如可以重定向到百度!
l 重定向:可以重定向到本項目內的其他資源,可以使用相對路徑,以“/項目名”開頭
l 重定向:會使瀏覽器的地址欄發生變化!
注意事項:
l 當response為以提交狀態,就不能再重定向了!
l 當使用了response的輸出流響應后,再重定向。如果沒有造成response提交,那么說明數據還在緩沖區中,tomcat會把緩沖區清空,然后重定向。
request
post請求方式
l 有主體(正文)
l 有Content-Type,表示主體的類型,默認值為application/x-www-form-urlencoded;
2 request功能:
l 可以獲取請求方式:String getMethod()
l 可以獲取請求頭:String getHeader(String name)
l 可以獲取請求參數(包含主體或路徑后面的參數):String getParameter(String name)
3 請求編碼
l 地址欄的參數是GBK的;
l 在頁面中點擊鏈接或提交表單,參數都由當前頁面的編碼來決定,而頁面的編碼由當初服務器響應的編碼來決定。
l 服務器請求form.html,服務器響應utf-8的頁面給瀏覽器,然后在form.html頁面上點擊鏈接和提交表單發送的參數都是utf-8。
l 如果服務器的所有頁面都是utf-8的,那么只要不在瀏覽器的地址欄中給出中文,那么其他的參數都是utf-8的。
服務器:
l 服務器默認使用ISO-8859-1來解讀請求數據。(tomcat7以前是這個編碼)
l 可以使用request.setCharacterEncoding(“utf-8”)設置編碼來解讀請求參數。這個方法只對請求主體有效,而GET請求沒有主體。說白了就是只對POST請求有效!
l 設置Tomcat 其中GET請求的默認編碼:
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8"/> |
l 因為編碼的設置不能依賴tomcat的配置,所以還是需要我們自己手動轉碼
Ø String name = request.getParamter(“username”);//使用默認的iso來解碼
Ø byte[] bytes = name.getBytes(“iso-8859-1”);//使用iso回退到字節數組
Ø name = new String(bytes, “utf-8”);//重新使用utf-8來解碼
4 獲取參數(詳細)
l *String getParameter(String name) :通過參數名稱獲取參數值!
l String[] getParameterValues(String name):通過參數名稱獲取多個參數值!一般復選框會出現一個名稱多個值的情況。
l *Map<String,String[]> getParameterMap():獲取所有參數,封裝到Map中,基參數名為key,參數值為value。
l Enumeration getParameterNames():獲取所有參數的名稱
5 request是Servlet三大域對象之
域功能:
l void setAttribute(String name,Object value)
l Object getAttribute(String name)
l void removeAttribute(String name)
request 的存儲范圍:整個請求鏈!如果一個請求經過了多個Servlet,那么這些Servlet可以共享request域!
6 request獲取頭信息
l String getHeader(String name):通過頭名稱,獲取頭信息;
l Enumeration getHeaderNames() :獲取所有頭的名稱;
l Enumeration getHeaders(String name):通過頭名稱,獲取多個頭信息;
l int getIntHeader(String name):通過頭名稱,獲取頭信息,本方法再把String的頭信息轉換成int類型。
7 reuqest的請求轉發
如何請求轉發
l 一個請求內經過多個資源(Servlet,還有jsp,而且經常是jsp)
l 請求轉發需要使用RequestDispatcher的forward(HttpServletRequest,HttpServletResponse)
l RequestDispatcher rd = request.getRequestDispatcher(“/BServlet”);//參數是要轉發的目標
l rd.forward(request,response);//轉發到BServlet
其實你可以理解成在一個Servlet中,調用另一個Servlet的service()方法。
請求轉發的注意事項
l 在第一個Servlet中可以使用request域保存數據,在第二個Servlet中可以使用request域獲取數據。因為這兩個Servlet共享同一個request對象。
l
l 在轉發語句之后,其他語句是否會執行?答案是“可以”!
l 不能在一個Servlet中即重定向,又轉發。
請求轉發與重定向比較
l 請求轉發后,地址欄中的地址不變!重定向變
l 請求轉發是一個請求,重定向是兩個請求;
l 請求轉發可以共享request域,而重定向因為是兩個請求,所以不能共享request。
l 一個請求,只有一個請求方式!所以轉發后還是原來的請求方式,如果一開始發出的是GET,那么整個請求都是GET!重定向不同,因為是多個請求,第一個無論是什么方式,第二個請求都是GET。
l 請轉轉發只能是本項目中的資源,而重定向可以其他項目。
如果要轉發,就不要輸出
l 如果輸出到緩沖區的數據,沒有提交,那么在轉發時,緩沖區會被清空,如果已經提交,那么在轉發時拋出異常。這一點與重定向相同!
l 留頭不留體:在第一個Servlet中設置頭沒問題,會保留到下一個Servlet。如果在第一個Servlet中輸出數據,即設置響應體,那么如果沒有提交,就被清空,如果已提交,就出異常。
8 請求包含
請求包含:
l RequestDispatcher rd = request.getRequestDispatcher(“/BServlet”);
l rd.include(request,response);
留頭又留體!
路徑
客戶端路徑:
1. 超鏈接:href=”/項目名/…”
2. 表單:action=”/項目名/…”
3. response.sendRedirect(“/項目名/…”);
如果客戶端路徑,沒有已“/項目名”開頭,那么相對的是當前頁面所在路徑。
例如:http://localhost:8080/day10_3/a.html,當前頁面所在路徑是http://localhost:8080/day10_3/
以“/”開頭的客戶端路徑相對“http://localhost:8080”,<a href=”/hello/AServlet”>
服務器端路徑:
轉發:必須使用“/”開頭,它相對當前項目,即http://localhost:8080/day10_3
包含:同上;
<url-pattern>:同上
ServletContext.getRealPath(“/a.jpg”):它是真對真實路徑,相對當前WebRoot
ServletContext.getResourceAsStream():同上
Class.getResourceAsStream():如果使用“/”開頭,相對classes,如果不使用“/”,相對當前.class文件所在目錄。
ClassLoader. getResourceAsStream():無論使用不使用“/”開頭,都相對classes
編碼:
URL編碼
作用:為了在客戶端與服務器之間傳遞中文!
把中文轉換成URL編碼:
Ø 首先你需要選擇一種字符編碼,然后把中文轉換成byte[]。
Ø 把每個字節轉換成16進制,前面添加上一個“%”。它不能顯負號,把得到的byte先加上128,這樣-128就是0了。正的127就是255了,它的范圍是%00~%FF
會話跟蹤技術Cookie &session
1 什么是會話跟蹤技術
我們需要先了解一下什么是會話!可以把會話理解為客戶端與服務器之間的一次會晤,在一次會晤中可能會包含多次請求和響應。例如你給10086打個電話,你就是客戶端,而10086服務人員就是服務器了。從雙方接通電話那一刻起,會話就開始了,到某一方掛斷電話表示會話結束。在通話過程中,你會向10086發出多個請求,那么這多個請求都在一個會話中。
在JavaWeb中,客戶向某一服務器發出第一個請求開始,會話就開始了,直到客戶關閉了瀏覽器會話結束。
在一個會話的多個請求中共享數據,這就是會話跟蹤技術。例如在一個會話中的請求如下:
l 請求銀行主頁;
l 請求登錄(請求參數是用戶名和密碼);
l 請求轉賬(請求參數與轉賬相關的數據);
l 請求信譽卡還款(請求參數與還款相關的數據)。
在這上會話中當前用戶信息必須在這個會話中共享的,因為登錄的是張三,那么在轉賬和還款時一定是相對張三的轉賬和還款!這就說明我們必須在一個會話過程中有共享數據的能力。
2 會話路徑技術使用Cookie或session完成
我們知道HTTP協議是無狀態協議,也就是說每個請求都是獨立的!無法記錄前一次請求的狀態。但HTTP協議中可以使用Cookie來完成會話跟蹤!
在JavaWeb中,使用session來完成會話跟蹤,session底層依賴Cookie技術。
Cookie
1 Cookie概述
1.1 什么叫Cookie
Cookie翻譯成中文是小甜點,小餅干的意思。在HTTP中它表示服務器送給客戶端瀏覽器的小甜點。其實Cookie就是一個鍵和一個值構成的,隨着服務器端的響應發送給客戶端瀏覽器。然后客戶端瀏覽器會把Cookie保存起來,當下一次再訪問服務器時把Cookie再發送給服務器。
Cookie是由服務器創建,然后通過響應發送給客戶端的一個鍵值對。客戶端會保存Cookie,並會標注出Cookie的來源(哪個服務器的Cookie)。當客戶端向服務器發出請求時會把所有這個服務器Cookie包含在請求中發送給服務器,這樣服務器就可以識別客戶端了!
1.2 Cookie規范
l Cookie大小上限為4KB;
l 一個服務器最多在客戶端瀏覽器上保存20個Cookie;
l 一個瀏覽器最多保存300個Cookie;
上面的數據只是HTTP的Cookie規范,但在瀏覽器大戰的今天,一些瀏覽器為了打敗對手,為了展現自己的能力起見,可能對Cookie規范“擴展”了一些,例如每個Cookie的大小為8KB,最多可保存500個Cookie等!但也不會出現把你硬盤占滿的可能!
注意,不同瀏覽器之間是不共享Cookie的。也就是說在你使用IE訪問服務器時,服務器會把Cookie發給IE,然后由IE保存起來,當你在使用FireFox訪問服務器時,不可能把IE保存的Cookie發送給服務器。
1.3 Cookie與HTTP頭
Cookie是通過HTTP請求和響應頭在客戶端和服務器端傳遞的:
l Cookie:請求頭,客戶端發送給服務器端;
Ø 格式:Cookie: a=A; b=B; c=C。即多個Cookie用分號離開;
l Set-Cookie:響應頭,服務器端發送給客戶端;
Ø 一個Cookie對象一個Set-Cookie:
Set-Cookie: a=A
Set-Cookie: b=B
Set-Cookie: c=C
1.4 Cookie的覆蓋
如果服務器端發送重復的Cookie那么會覆蓋原有的Cookie,例如客戶端的第一個請求服務器端發送的Cookie是:Set-Cookie: a=A;第二請求服務器端發送的是:Set-Cookie: a=AA,那么客戶端只留下一個Cookie,即:a=AA。
1.5 Cookie第一例
我們這個案例是,客戶端訪問AServlet,AServlet在響應中添加Cookie,瀏覽器會自動保存Cookie。然后客戶端訪問BServlet,這時瀏覽器會自動在請求中帶上Cookie,BServlet獲取請求中的Cookie打印出來。
AServlet.java
package cn.itcast.servlet; import java.io.IOException; import java.util.UUID; import javax.servlet.ServletException; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 給客戶端發送Cookie * @author Administrator * */ public class AServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=utf-8"); String id = UUID.randomUUID().toString();//生成一個隨機字符串 Cookie cookie = new Cookie("id", id);//創建Cookie對象,指定名字和值 response.addCookie(cookie);//在響應中添加Cookie對象 response.getWriter().print("已經給你發送了ID"); } } |
BServlet.java
package cn.itcast.servlet; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 獲取客戶端請求中的Cookie * @author Administrator * */ public class BServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=utf-8"); Cookie[] cs = request.getCookies();//獲取請求中的Cookie if(cs != null) {//如果請求中存在Cookie for(Cookie c : cs) {//遍歷所有Cookie if(c.getName().equals("id")) {//獲取Cookie名字,如果Cookie名字是id response.getWriter().print("您的ID是:" + c.getValue());//打印Cookie值 } } } } } |
2 Cookie的生命
2.1 什么是Cookie的生命
Cookie不只是有name和value,Cookie還是生命。所謂生命就是Cookie在客戶端的有效時間,可以通過setMaxAge(int)來設置Cookie的有效時間。
l cookie.setMaxAge(-1):cookie的maxAge屬性的默認值就是-1,表示只在瀏覽器內存中存活。一旦關閉瀏覽器窗口,那么cookie就會消失。
l cookie.setMaxAge(60*60):表示cookie對象可存活1小時。當生命大於0時,瀏覽器會把Cookie保存到硬盤上,就算關閉瀏覽器,就算重啟客戶端電腦,cookie也會存活1小時;
l cookie.setMaxAge(0):cookie生命等於0是一個特殊的值,它表示cookie被作廢!也就是說,如果原來瀏覽器已經保存了這個Cookie,那么可以通過Cookie的setMaxAge(0)來刪除這個Cookie。無論是在瀏覽器內存中,還是在客戶端硬盤上都會刪除這個Cookie。
2.2 瀏覽器查看Cookie
下面是瀏覽器查看Cookie的方式:
l IE查看Cookie文件的路徑:C:\Documents and Settings\Administrator\Cookies;
l FireFox查看Cooke:
l Google查看Cookie:
2.3 案例:顯示上次訪問時間
l 創建Cookie,名為lasttime,值為當前時間,添加到response中;
l 在AServlet中獲取請求中名為lasttime的Cookie;
l 如果不存在輸出“您是第一次訪問本站”,如果存在輸出“您上一次訪問本站的時間是xxx”;
AServlet.java
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=utf-8"); Cookie cookie = new Cookie("lasttime", new Date().toString()); cookie.setMaxAge(60 * 60); response.addCookie(cookie); Cookie[] cs = request.getCookies(); String s = "您是首次訪問本站!"; if(cs != null) { for(Cookie c : cs) { if(c.getName().equals("lasttime")) { s = "您上次的訪問時間是:" + c.getValue(); } } } response.getWriter().print(s); } |
3 Cookie的path
3.1 什么是Cookie的路徑
現在有WEB應用A,向客戶端發送了10個Cookie,這就說明客戶端無論訪問應用A的哪個Servlet都會把這10個Cookie包含在請求中!但是也許只有AServlet需要讀取請求中的Cookie,而其他Servlet根本就不會獲取請求中的Cookie。這說明客戶端瀏覽器有時發送這些Cookie是多余的!
可以通過設置Cookie的path來指定瀏覽器,在訪問什么樣的路徑時,包含什么樣的Cookie。
3.2 Cookie路徑與請求路徑的關系
下面我們來看看Cookie路徑的作用:
下面是客戶端瀏覽器保存的3個Cookie的路徑:
a: /cookietest;
b: /cookietest/servlet;
c: /cookietest/jsp;
下面是瀏覽器請求的URL:
A: http://localhost:8080/cookietest/AServlet;
B: http://localhost:8080/cookietest/servlet/BServlet;
C: http://localhost:8080/cookietest/jsp/CServlet;
l 請求A時,會在請求中包含a;
l 請求B時,會在請求中包含a、b;
l 請求C時,會在請求中包含a、c;
也就是說,請求路徑如果包含了Cookie路徑,那么會在請求中包含這個Cookie,否則不會請求中不會包含這個Cookie。
l A請求的URL包含了“/cookietest”,所以會在請求中包含路徑為“/cookietest”的Cookie;
l B請求的URL包含了“/cookietest”,以及“/cookietest/servlet”,所以請求中包含路徑為“/cookietest”和“/cookietest/servlet”兩個Cookie;
l B請求的URL包含了“/cookietest”,以及“/cookietest/jsp”,所以請求中包含路徑為“/cookietest”和“/cookietest/jsp”兩個Cookie;
3.3 設置Cookie的路徑
設置Cookie的路徑需要使用setPath()方法,例如:
cookie.setPath(“/cookietest/servlet”);
如果沒有設置Cookie的路徑,那么Cookie路徑的默認值當前訪問資源所在路徑,例如:
l 訪問http://localhost:8080/cookietest/AServlet時添加的Cookie默認路徑為/cookietest;
l 訪問http://localhost:8080/cookietest/servlet/BServlet時添加的Cookie默認路徑為/cookietest/servlet;
l 訪問http://localhost:8080/cookietest/jsp/BServlet時添加的Cookie默認路徑為/cookietest/jsp;
4 Cookie的domain
Cookie的domain屬性可以讓網站中二級域共享Cookie,次要!
百度你是了解的對吧!
http://www.baidu.com
http://zhidao.baidu.com
http://news.baidu.com
http://tieba.baidu.com
現在我希望在這些主機之間共享Cookie(例如在www.baidu.com中響應的cookie,可以在news.baidu.com請求中包含)。很明顯,現在不是路徑的問題了,而是主機的問題,即域名的問題。處理這一問題其實很簡單,只需要下面兩步:
l 設置Cookie的path為“/”:c.setPath(“/”);
l 設置Cookie的domain為“.baidu.com”:c.setDomain(“.baidu.com”)。
當domain為“.baidu.com”時,無論前綴是什么,都會共享Cookie的。但是現在我們需要設置兩個虛擬主機:www.baidu.com和news.baidu.com。
第一步:設置windows的DNS路徑解析
找到C:\WINDOWS\system32\drivers\etc\hosts文件,添加如下內容
127.0.0.1 localhost 127.0.0.1 www.baidu.com 127.0.0.1 news.baidu.com |
第二步:設置Tomcat虛擬主機
找到server.xml文件,添加<Host>元素,內容如下:
<Host name="www.baidu.com" appBase="F:\webapps\www" unpackWARs="true" autoDeploy="true" xmlValidation="false" xmlNamespaceAware="false"/> <Host name="news.baidu.com" appBase="F:\webapps\news" unpackWARs="true" autoDeploy="true" xmlValidation="false" xmlNamespaceAware="false"/> |
第三步:創建A項目,創建AServlet,設置Cookie。
Cookie c = new Cookie("id", "baidu"); c.setPath("/"); c.setDomain(".baidu.com"); c.setMaxAge(60*60); response.addCookie(c); response.getWriter().print("OK"); |
把A項目的WebRoot目錄復制到F:\webapps\www目錄下,並把WebRoot目錄的名字修改為ROOT。
第四步:創建B項目,創建BServlet,獲取Cookie,並打印出來。
Cookie[] cs = request.getCookies(); if(cs != null) { for(Cookie c : cs) { String s = c.getName() + ": " + c.getValue() + "<br/>"; response.getWriter().print(s); } } |
把B項目的WebRoot目錄復制到F:\webapps\news目錄下,並把WebRoot目錄的名字修改為ROOT。
第五步:訪問www.baidu.com\AServlet,然后再訪問news.baidu.com\BServlet。
5 Cookie中保存中文
Cookie的name和value都不能使用中文,如果希望在Cookie中使用中文,那么需要先對中文進行URL編碼,然后把編碼后的字符串放到Cookie中。
向客戶端響應中添加Cookie
String name = URLEncoder.encode("姓名", "UTF-8"); String value = URLEncoder.encode("張三", "UTF-8"); Cookie c = new Cookie(name, value); c.setMaxAge(3600); response.addCookie(c); |
從客戶端請求中獲取Cookie
response.setContentType("text/html;charset=utf-8"); Cookie[] cs = request.getCookies(); if(cs != null) { for(Cookie c : cs) { String name = URLDecoder.decode(c.getName(), "UTF-8"); String value = URLDecoder.decode(c.getValue(), "UTF-8"); String s = name + ": " + value + "<br/>"; response.getWriter().print(s); } } |
6 顯示曾經瀏覽過的商品
index.jsp
<body> <h1>商品列表</h1> <a href="/day06_3/GoodServlet?name=ThinkPad">ThinkPad</a><br/> <a href="/day06_3/GoodServlet?name=Lenovo">Lenovo</a><br/> <a href="/day06_3/GoodServlet?name=Apple">Apple</a><br/> <a href="/day06_3/GoodServlet?name=HP">HP</a><br/> <a href="/day06_3/GoodServlet?name=SONY">SONY</a><br/> <a href="/day06_3/GoodServlet?name=ACER">ACER</a><br/> <a href="/day06_3/GoodServlet?name=DELL">DELL</a><br/> <hr/> 您瀏覽過的商品: <% Cookie[] cs = request.getCookies(); if(cs != null) { for(Cookie c : cs) { if(c.getName().equals("goods")) { out.print(c.getValue()); } } } %> </body> |
GoodServlet
public class GoodServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String goodName = request.getParameter("name"); String goods = CookieUtils.getCookValue(request, "goods"); if(goods != null) { String[] arr = goods.split(", "); Set<String> goodSet = new LinkedHashSet(Arrays.asList(arr)); goodSet.add(goodName); goods = goodSet.toString(); goods = goods.substring(1, goods.length() - 1); } else { goods = goodName; } Cookie cookie = new Cookie("goods", goods); cookie.setMaxAge(1 * 60 * 60 * 24); response.addCookie(cookie); response.sendRedirect("/day06_3/index.jsp"); } } |
CookieUtils
public class CookieUtils { public static String getCookValue(HttpServletRequest request, String name) { Cookie[] cs = request.getCookies(); if(cs == null) { return null; } for(Cookie c : cs) { if(c.getName().equals(name)) { return c.getValue(); } } return null; } } |
HttpSession
HttpSession概述
1.1 什么是HttpSesssion
javax.servlet.http.HttpSession接口表示一個會話,我們可以把一個會話內需要共享的數據保存到HttSession對象中!
1.2 獲取HttpSession對象
l HttpSession request.getSesssion():如果當前會話已經有了session對象那么直接返回,如果當前會話還不存在會話,那么創建session並返回;
l HttpSession request.getSession(boolean):當參數為true時,與requeset.getSession()相同。如果參數為false,那么如果當前會話中存在session則返回,不存在返回null;
1.3 HttpSession是域對象
我們已經學習過HttpServletRequest、ServletContext,它們都是域對象,現在我們又學習了一個HttpSession,它也是域對象。它們三個是Servlet中可以使用的域對象,而JSP中可以多使用一個域對象,明天我們再講解JSP的第四個域對象。
l HttpServletRequest:一個請求創建一個request對象,所以在同一個請求中可以共享request,例如一個請求從AServlet轉發到BServlet,那么AServlet和BServlet可以共享request域中的數據;
l ServletContext:一個應用只創建一個ServletContext對象,所以在ServletContext中的數據可以在整個應用中共享,只要不啟動服務器,那么ServletContext中的數據就可以共享;
l HttpSession:一個會話創建一個HttpSession對象,同一會話中的多個請求中可以共享session中的數據;
下載是session的域方法:
l void setAttribute(String name, Object value):用來存儲一個對象,也可以稱之為存儲一個域屬性,例如:session.setAttribute(“xxx”, “XXX”),在session中保存了一個域屬性,域屬性名稱為xxx,域屬性的值為XXX。請注意,如果多次調用該方法,並且使用相同的name,那么會覆蓋上一次的值,這一特性與Map相同;
l Object getAttribute(String name):用來獲取session中的數據,當前在獲取之前需要先去存儲才行,例如:String value = (String) session.getAttribute(“xxx”);,獲取名為xxx的域屬性;
l void removeAttribute(String name):用來移除HttpSession中的域屬性,如果參數name指定的域屬性不存在,那么本方法什么都不做;
l Enumeration getAttributeNames():獲取所有域屬性的名稱;
2 登錄案例
需要的頁面:
l login.jsp:登錄頁面,提供登錄表單;
l index1.jsp:主頁,顯示當前用戶名稱,如果沒有登錄,顯示您還沒登錄;
l index2.jsp:主頁,顯示當前用戶名稱,如果沒有登錄,顯示您還沒登錄;
Servlet:
l LoginServlet:在login.jsp頁面提交表單時,請求本Servlet。在本Servlet中獲取用戶名、密碼進行校驗,如果用戶名、密碼錯誤,顯示“用戶名或密碼錯誤”,如果正確保存用戶名session中,然后重定向到index1.jsp;
當用戶沒有登錄時訪問index1.jsp或index2.jsp,顯示“您還沒有登錄”。如果用戶在login.jsp登錄成功后到達index1.jsp頁面會顯示當前用戶名,而且不用再次登錄去訪問index2.jsp也會顯示用戶名。因為多次請求在一個會話范圍,index1.jsp和index2.jsp都會到session中獲取用戶名,session對象在一個會話中是相同的,所以都可以獲取到用戶名!
login.jsp
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title>login.jsp</title> </head> <body> <h1>login.jsp</h1> <hr/> <form action="/day06_4/LoginServlet" method="post"> 用戶名:<input type="text" name="username" /><br/> <input type="submit" value="Submit"/> </form> </body> </html> |
index1.jsp
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title>index1.jsp</title> </head> <body> <h1>index1.jsp</h1> <% String username = (String)session.getAttribute("username"); if(username == null) { out.print("您還沒有登錄!"); } else { out.print("用戶名:" + username); } %> <hr/> <a href="/day06_4/index2.jsp">index2</a> </body> </html> |
index2.jsp
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title>index2.jsp</title> </head> <body> <h1>index2.jsp</h1> <% String username = (String)session.getAttribute("username"); if(username == null) { out.print("您還沒有登錄!"); } else { out.print("用戶名:" + username); } %> <hr/> <a href="/day06_4/index1.jsp">index1</a> </body> </html> |
LoginServlet
public class LoginServlet extends HttpServlet { public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("utf-8"); response.setContentType("text/html;charset=utf-8"); String username = request.getParameter("username"); if(username.equalsIgnoreCase("itcast")) { response.getWriter().print("用戶名或密碼錯誤!"); } else { HttpSession session = request.getSession(); session.setAttribute("username", username); response.sendRedirect("/day06_4/index1.jsp"); } } } |
3 session的實現原理
session底層是依賴Cookie的!我們來理解一下session的原理吧!
當我首次去銀行時,因為還沒有賬號,所以需要開一個賬號,我獲得的是銀行卡,而銀行這邊的數據庫中留下了我的賬號,我的錢是保存在銀行的賬號中,而我帶走的是我的卡號。
當我再次去銀行時,只需要帶上我的卡,而無需再次開一個賬號了。只要帶上我的卡,那么我在銀行操作的一定是我的賬號!
當首次使用session時,服務器端要創建session,session是保存在服務器端,而給客戶端的session的id(一個cookie中保存了sessionId)。客戶端帶走的是sessionId,而數據是保存在session中。
當客戶端再次訪問服務器時,在請求中會帶上sessionId,而服務器會通過sessionId找到對應的session,而無需再創建新的session。
4 session與瀏覽器
session保存在服務器,而sessionId通過Cookie發送給客戶端,但這個Cookie的生命不-1,即只在瀏覽器內存中存在,也就是說如果用戶關閉了瀏覽器,那么這個Cookie就丟失了。
當用戶再次打開瀏覽器訪問服務器時,就不會有sessionId發送給服務器,那么服務器會認為你沒有session,所以服務器會創建一個session,並在響應中把sessionId中到Cookie中發送給客戶端。
你可能會說,那原來的session對象會怎樣?當一個session長時間沒人使用的話,服務器會把session刪除了!這個時長在Tomcat中配置是30分鍾,可以在${CATALANA}/conf/web.xml找到這個配置,當然你也可以在自己的web.xml中覆蓋這個配置!
web.xml
<session-config> <session-timeout>30</session-timeout> </session-config> |
session失效時間也說明一個問題!如果你打開網站的一個頁面開始長時間不動,超出了30分鍾后,再去點擊鏈接或提交表單時你會發現,你的session已經丟失了!
5 session其他常用API
l String getId():獲取sessionId;
l int getMaxInactiveInterval():獲取session可以的最大不活動時間(秒),默認為30分鍾。當session在30分鍾內沒有使用,那么Tomcat會在session池中移除這個session;
l void setMaxInactiveInterval(int interval):設置session允許的最大不活動時間(秒),如果設置為1秒,那么只要session在1秒內不被使用,那么session就會被移除;
l long getCreationTime():返回session的創建時間,返回值為當前時間的毫秒值;
l long getLastAccessedTime():返回session的最后活動時間,返回值為當前時間的毫秒值;
l void invalidate():讓session失效!調用這個方法會被session失效,當session失效后,客戶端再次請求,服務器會給客戶端創建一個新的session,並在響應中給客戶端新session的sessionId;
l boolean isNew():查看session是否為新。當客戶端第一次請求時,服務器為客戶端創建session,但這時服務器還沒有響應客戶端,也就是還沒有把sessionId響應給客戶端時,這時session的狀態為新。
6 URL重寫
我們知道session依賴Cookie,那么session為什么依賴Cookie呢?因為服務器需要在每次請求中獲取sessionId,然后找到客戶端的session對象。那么如果客戶端瀏覽器關閉了Cookie呢?那么session是不是就會不存在了呢?
其實還有一種方法讓服務器收到的每個請求中都帶有sessioinId,那就是URL重寫!在每個頁面中的每個鏈接和表單中都添加名為jSessionId的參數,值為當前sessionid。當用戶點擊鏈接或提交表單時也服務器可以通過獲取jSessionId這個參數來得到客戶端的sessionId,找到sessoin對象。
index.jsp
<body> <h1>URL重寫</h1> <a href='/day06_5/index.jsp;jsessionid=<%=session.getId() %>' >主頁</a> <form action='/day06_5/index.jsp;jsessionid=<%=session.getId() %>' method="post"> <input type="submit" value="提交"/> </form> </body> |
也可以使用response.encodeURL()對每個請求的URL處理,這個方法會自動追加jsessionid參數,與上面我們手動添加是一樣的效果。
<a href='<%=response.encodeURL("/day06_5/index.jsp") %>' >主頁</a> <form action='<%=response.encodeURL("/day06_5/index.jsp") %>' method="post"> <input type="submit" value="提交"/> </form> |
使用response.encodeURL()更加“智能”,它會判斷客戶端瀏覽器是否禁用了Cookie,如果禁用了,那么這個方法在URL后面追加jsessionid,否則不會追加。
案例:一次性圖片驗證碼
1 驗證碼有啥用
在我們注冊時,如果沒有驗證碼的話,我們可以使用URLConnection來寫一段代碼發出注冊請求。甚至可以使用while(true)來注冊!那么服務器就廢了!
驗證碼可以去識別發出請求的是人還是程序!當然,如果聰明的程序可以去分析驗證碼圖片!但分析圖片也不是一件容易的事,因為一般驗證碼圖片都會帶有干擾線,人都看不清,那么程序一定分析不出來。
2 VerifyCode類
現在我們已經有了cn.itcast.utils.VerifyCode類,這個類可以生成驗證碼圖片!下面來看一個小例子。
public void fun1() throws IOException { // 創建驗證碼類 VerifyCode vc = new VerifyCode(); // 獲取隨機圖片 BufferedImage image = vc.getImage(); // 獲取剛剛生成的隨機圖片上的文本 String text = vc.getText(); System.out.println(text); // 保存圖片 FileOutputStream out = new FileOutputStream("F:/xxx.jpg"); VerifyCode.output(image, out); } |
3 在頁面中顯示動態圖片
我們需要寫一個VerifyCodeServlet,在這個Servlet中我們生成動態圖片,然后它圖片寫入到response.getOutputStream()流中!然后讓頁面的<img>元素指定這個VerifyCodServlet即可。
VerifyCodeServlet
public class VerifyCodeServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { VerifyCode vc = new VerifyCode(); BufferedImage image = vc.getImage(); String text = vc.getText(); System.out.println("text:" + text); VerifyCode.output(image, response.getOutputStream()); } } |
index.jsp
<script type="text/javascript"> function _change() { var imgEle = document.getElementById("vCode"); imgEle.src = "/day06_6/VerifyCodeServlet?" + new Date().getTime(); } </script> ... <body> <h1>驗證碼</h1> <img id="vCode" src="/day06_6/VerifyCodeServlet"/> <a href="javascript:_change()">看不清,換一張</a> </body> |
4 在注冊頁面中使用驗證碼
<form action="/day06_6/RegistServlet" method="post"> 用戶名:<input type="text" name="username"/><br/> 驗證碼:<input type="text" name="code" size="3"/> <img id="vCode" src="/day06_6/VerifyCodeServlet"/> <a href="javascript:_change()">看不清,換一張</a> <br/> <input type="submit" value="Submit"/> </form> |
5 RegistServlet
修改VerifyCodeServlet
public class VerifyCodeServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { VerifyCode vc = new VerifyCode(); BufferedImage image = vc.getImage(); request.getSession().setAttribute("vCode", vc.getText()); VerifyCode.output(image, response.getOutputStream()); } } |
RegistServlet
public class RegistServlet extends HttpServlet { public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("utf-8"); response.setContentType("text/html;charset=utf-8"); String username = request.getParameter("username"); String vCode = request.getParameter("code"); String sessionVerifyCode = (String)request.getSession().getAttribute("vCode"); if(vCode.equalsIgnoreCase(sessionVerifyCode)) { response.getWriter().print(username + ", 恭喜!注冊成功!"); } else { response.getWriter().print("驗證碼錯誤!"); } } } |
6 總結驗證碼案例
l VerifyCodeServlet:
Ø 生成驗證碼:VerifyCode vc = new VerifyCode(); BufferedImage image = vc.getImage();
Ø 在session中保存驗證碼文本:request.getSession.getAttribute(“vCode”, vc.getText());
Ø 把驗證碼輸出到頁面:VerifyCode.output(image, response.getOutputStream);
l regist.jsp:
Ø 表單中包含username和code字段;
Ø 在表單中給出<img>指向VerifyCodeServlet,用來在頁面中顯示驗證碼圖片;
Ø 提供“看不清,換一張”鏈接,指向_change()函數;
Ø 提交到RegistServlet;
l RegistServlet:
Ø 獲取表單中的username和code;
Ø 獲取session中的vCode;
Ø 比較code和vCode是否相同;
Ø 相同說明用戶輸入的驗證碼正確,否則輸入驗證碼錯誤。
Jsp&el表達式
JSP指令
JSP指令概述
JSP指令的格式:<%@指令名 attr1=”” attr2=”” %>,一般都會把JSP指令放到JSP文件的最上方,但這不是必須的。
JSP中有三大指令:page、include、taglib,最為常用,也最為復雜的就是page指令了。
2 page指令
page指令是最為常用的指定,也是屬性最多的屬性!
page指令沒有必須屬性,都是可選屬性。例如<%@page %>,沒有給出任何屬性也是可以的!
在JSP頁面中,任何指令都可以重復出現!
<%@ page language=”java”%>
<%@ page import=”java.util.*”%>
<%@ page pageEncoding=”utf-8”%>
這也是可以的!
2.1 page指令的pageEncoding和contentType(重點)
pageEncoding指定當前JSP頁面的編碼!這個編碼是給服務器看的,服務器需要知道當前JSP使用的編碼,不然服務器無法正確把JSP編譯成java文件。所以這個編碼只需要與真實的頁面編碼一致即可!在MyEclipse中,在JSP文件上點擊右鍵,選擇屬性就可以看到當前JSP頁面的編碼了。
contentType屬性與response.setContentType()方法的作用相同!它會完成兩項工作,一是設置響應字符流的編碼,二是設置content-type響應頭。例如:<%@ contentType=”text/html;charset=utf-8”%>,它會使“真身”中出現response.setContentType(“text/html;charset=utf-8”)。
無論是page指令的pageEncoding還是contentType,它們的默認值都是ISO-8859-1,我們知道ISO-8859-1是無法顯示中文的,所以JSP頁面中存在中文的話,一定要設置這兩個屬性。
其實pageEncoding和contentType這兩個屬性的關系很“曖昧”:
l 當設置了pageEncoding,而沒設置contentType時: contentType的默認值為pageEncoding;
l 當設置了contentType,而沒設置pageEncoding時: pageEncoding的默認值與contentType;
也就是說,當pageEncoding和contentType只出現一個時,那么另一個的值與出現的值相同。如果兩個都不出現,那么兩個屬性的值都是ISO-8859-1。所以通過我們至少設置它們兩個其中一個!
2.2 page指令的import屬性
import是page指令中一個很特別的屬性!
import屬性值對應“真身”中的import語句。
import屬性值可以使逗號:<%@page import=”java.net.*,java.util.*,java.sql.*”%>
import屬性是唯一可以重復出現的屬性:
<%@page import=”java.util.*” import=”java.net.*” import=”java.sql.*”%>
但是,我們一般會使用多個page指令來導入多個包:
<%@ page import=”java.util.*”%>
<%@ page import=”java.net.*”%>
<%@ page import=”java.text.*”%>
2.3 page指令的errorPage和isErrorPage
我們知道,在一個JSP頁面出錯后,Tomcat會響應給用戶錯誤信息(500頁面)!如果你不希望Tomcat給用戶輸出錯誤信息,那么可以使用page指令的errorPage來指定自己的錯誤頁!也就是自定義錯誤頁面,例如:<%@page errorPage=”xxx.jsp”%>。這時,在當前JSP頁面出現錯誤時,會請求轉發到xxx.jsp頁面。
a.jsp
<%@ page import="java.util.*" pageEncoding="UTF-8"%> <%@ page errorPage="b.jsp" %> <% if(true) throw new Exception("哈哈~"); %> |
b.jsp
<%@ page pageEncoding="UTF-8"%> <body> <h1>出錯啦!</h1> </body> </html> |
在上面代碼中,a.jsp拋出異常后,會請求轉發到b.jsp。在瀏覽器的地址欄中還是a.jsp,因為是請求轉發!
而且客戶端瀏覽器收到的響應碼為200,表示請求成功!如果希望客戶端得到500,那么需要指定b.jsp為錯誤頁面。
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <%@ page isErrorPage="true" %> <html> <body> <h1>出錯啦!</h1> <%=exception.getMessage() %> </body> </html> |
注意,當isErrorPage為true時,說明當前JSP為錯誤頁面,即專門處理錯誤的頁面。那么這個頁面中就可以使用一個內置對象exception了。其他頁面是不能使用這個內置對象的!
溫馨提示:IE會在狀態碼為500時,並且響應正文的長度小於等於512B時不給予顯示!而是顯示“網站無法顯示該頁面”字樣。這時你只需要添加一些響應內容即可,例如上例中的b.jsp中我給出一些內容,IE就可以正常顯示了!
2.3.1 web.xml中配置錯誤頁面
不只可以通過JSP的page指令來配置錯誤頁面,還可以在web.xml文件中指定錯誤頁面。這種方式其實與page指令無關,但想來想去還是在這個位置來講解比較合適!
web.xml
<error-page> <error-code>404</error-code> <location>/error404.jsp</location> </error-page> <error-page> <error-code>500</error-code> <location>/error500.jsp</location> </error-page> <error-page> <exception-type>java.lang.RuntimeException</exception-type> <location>/error.jsp</location> </error-page> |
<error-page>有兩種使用方式:
l <error-code>和<location>子元素;
l <exception-type>和<location>子元素;
其中<error-code>是指定響應碼;<location>指定轉發的頁面;<exception-type>是指定拋出的異常類型。
在上例中:
l 當出現404時,會跳轉到error404.jsp頁面;
l 當出現RuntimeException異常時,會跳轉到error.jsp頁面;
l 當出現非RuntimeException的異常時,會跳轉到error500.jsp頁面。
這種方式會在控制台看到異常信息!而使用page指令時不會在控制台打印異常信息。
2.4 page指令的autFlush和buffer(不重要)
buffer表示當前JSP的輸出流(out隱藏對象)的緩沖區大小,默認為8kb。
authFlush表示在out對象的緩沖區滿時如果處理!當authFlush為true時,表示緩沖區滿時把緩沖區數據輸出到客戶端;當authFlush為false時,表示緩沖區滿時,拋出異常。authFlush的默認值為true。
這兩個屬性一般我們也不會去特意設置,都是保留默認值!
2.5 page指令的isELIgnored
后面我們會講解EL表達式語言,page指令的isElIgnored屬性表示當前JSP頁面是否忽略EL表達式,默認值為false,表示不忽略(即支持)。
2.6 page指令的其他屬性(更不重要)
l language:只能是Java,這個屬性可以看出JSP最初設計時的野心!希望JSP可以轉換成其他語言!但是,到現在JSP也只能轉換成Java代碼;
l info:JSP說明性信息;
l isThreadSafe:默認為false,為true時,JSP生成的Servlet會去實現一個過時的標記接口SingleThreadModel,這時JSP就只能處理單線程的訪問;
l session:默認為true,表示當前JSP頁面可以使用session對象,如果為false表示當前JSP頁面不能使用session對象;
l extends:指定當前JSP頁面生成的Servlet的父類;
2.7 <jsp-config>(了解)
在web.xml頁面中配置<jsp-config>也可以完成很多page指定的功能!
<jsp-config> <jsp-property-group> <url-pattern>*.jsp</url-pattern> <el-ignored>true</el-ignored> <page-encoding>UTF-8</page-encoding> <scripting-invalid>true</scripting-invalid> </jsp-property-group> </jsp-config> |
3 include指令
include指令表示靜態包含!即目的是把多個JSP合並成一個JSP文件!
include指令只有一個屬性:file,指定要包含的頁面,例如:<%@include file=”b.jsp”%>。
靜態包含:當hel.jsp頁面包含了lo.jsp頁面后,在編譯hel.jsp頁面時,需要把hel.jsp和lo.jsp頁面合並成一個文件,然后再編譯成Servlet(Java文件)。
很明顯,在ol.jsp中在使用username變量,而這個變量在hel.jsp中定義的,所以只有這兩個JSP文件合並后才能使用。通過include指定完成對它們的合並!
4 taglib指令
這個指令需要在學習了自定義標簽后才會使用,現在只能做了了解而已!
在JSP頁面中使用第三方的標簽庫時,需要使用taglib指令來“導包”。例如:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
其中prefix表示標簽的前綴,這個名稱可以隨便起。uri是由第三方標簽庫定義的,所以你需要知道第三方定義的uri。
JSP九大內置對象
1 什么是JSP九大內置對象
在JSP中無需創建就可以使用的9個對象,它們是:
l out(JspWriter):等同與response.getWriter(),用來向客戶端發送文本數據;
l config(ServletConfig):對應“真身”中的ServletConfig;
l page(當前JSP的真身類型):當前JSP頁面的“this”,即當前對象;
l pageContext(PageContext):頁面上下文對象,它是最后一個沒講的域對象;
l exception(Throwable):只有在錯誤頁面中可以使用這個對象;
l request(HttpServletRequest):即HttpServletRequest類的對象;
l response(HttpServletResponse):即HttpServletResponse類的對象;
l application(ServletContext):即ServletContext類的對象;
l session(HttpSession):即HttpSession類的對象,不是每個JSP頁面中都可以使用,如果在某個JSP頁面中設置<%@page session=”false”%>,說明這個頁面不能使用session。
在這9個對象中有很多是極少會被使用的,例如:config、page、exception基本不會使用。
在這9個對象中有兩個對象不是每個JSP頁面都可以使用的:exception、session。
在這9個對象中有很多前面已經學過的對象:out、request、response、application、session、config。
2 通過“真身”來對照JSP
我們知道JSP頁面的內容出現在“真身”的_jspService()方法中,而在_jspService()方法開頭部分已經創建了9大內置對象。
public void _jspService(HttpServletRequest request, HttpServletResponse response) throws java.io.IOException, ServletException { PageContext pageContext = null; HttpSession session = null; ServletContext application = null; ServletConfig config = null; JspWriter out = null; Object page = this; JspWriter _jspx_out = null; PageContext _jspx_page_context = null; try { response.setContentType("text/html;charset=UTF-8"); pageContext = _jspxFactory.getPageContext(this, request, response, null, true, 8192, true); _jspx_page_context = pageContext; application = pageContext.getServletContext(); config = pageContext.getServletConfig(); session = pageContext.getSession(); out = pageContext.getOut(); _jspx_out = out; 從這里開始,才是JSP頁面的內容 }… |
3 pageContext對象
在JavaWeb中一共四個域對象,其中Servlet中可以使用的是request、session、application三個對象,而在JSP中可以使用pageContext、request、session、application四個域對象。
pageContext 對象是PageContext類型,它的主要功能有:
l 域對象功能;
l 代理其它域對象功能;
l 獲取其他內置對象;
3.1 域對象功能
pageContext也是域對象,它的范圍是當前頁面。它的范圍也是四個域對象中最小的!
l void setAttribute(String name, Object value);
l Object getAttrbiute(String name, Object value);
l void removeAttribute(String name, Object value);
3.2 代理其它域對象功能
還可以使用pageContext來代理其它3個域對象的功能,也就是說可以使用pageContext向request、session、application對象中存取數據,例如:
pageContext.setAttribute("x", "X"); pageContext.setAttribute("x", "XX", PageContext.REQUEST_SCOPE); pageContext.setAttribute("x", "XXX", PageContext.SESSION_SCOPE); pageContext.setAttribute("x", "XXXX", PageContext.APPLICATION_SCOPE); |
l void setAttribute(String name, Object value, int scope):在指定范圍中添加數據;
l Object getAttribute(String name, int scope):獲取指定范圍的數據;
l void removeAttribute(String name, int scope):移除指定范圍的數據;
l Object findAttribute(String name):依次在page、request、session、application范圍查找名稱為name的數據,如果找到就停止查找。這說明在這個范圍內有相同名稱的數據,那么page范圍的優先級最高!
3.3 獲取其他內置對象
一個pageContext對象等於所有內置對象,即1個當9個。這是因為可以使用pageContext對象獲取其它8個內置對象:
l JspWriter getOut():獲取out內置對象;
l ServletConfig getServletConfig():獲取config內置對象;
l Object getPage():獲取page內置對象;
l ServletRequest getRequest():獲取request內置對象;
l ServletResponse getResponse():獲取response內置對象;
l HttpSession getSession():獲取session內置對象;
l ServletContext getServletContext():獲取application內置對象;
l Exception getException():獲取exception內置對象;
JSP動作標簽
1 JSP動作標簽概述
動作標簽的作用是用來簡化Java腳本的!
JSP動作標簽是JavaWeb內置的動作標簽,它們是已經定義好的動作標簽,我們可以拿來直接使用。
如果JSP動作標簽不夠用時,還可以使用自定義標簽(今天不講)。JavaWeb一共提供了20個JSP動作標簽,但有很多基本沒有用,這里只介紹一些有坐標的動作標簽。
JSP動作標簽的格式:<jsp:標簽名 …>
2 <jsp:include>
<jsp:include>標簽的作用是用來包含其它JSP頁面的!你可能會說,前面已經學習了include指令了,它們是否相同呢?雖然它們都是用來包含其它JSP頁面的,但它們的實現的級別是不同的!
include指令是在編譯級別完成的包含,即把當前JSP和被包含的JSP合並成一個JSP,然后再編譯成一個Servlet。
include動作標簽是在運行級別完成的包含,即當前JSP和被包含的JSP都會各自生成Servlet,然后在執行當前JSP的Servlet時完成包含另一個JSP的Servlet。它與RequestDispatcher的include()方法是相同的!
hel.jsp
<body> <h1>hel.jsp</h1> <jsp:include page="lo.jsp" /> </body> |
lo.jsp
<% out.println("<h1>lo.jsp</h1>"); %> |
其實<jsp:include>在“真身”中不過是一句方法調用,即調用另一個Servlet而已。
3 <jsp:forward>
forward標簽的作用是請求轉發!forward標簽的作用與RequestDispatcher#forward()方法相同。
hel.jsp
lo.jsp
<% out.println("<h1>lo.jsp</h1>"); %> |
注意,最后客戶端只能看到lo.jsp的輸出,而看不到hel.jsp的內容。也就是說在hel.jsp中的<h1>hel.jsp</h1>是不會發送到客戶端的。<jsp:forward>的作用是“別在顯示我,去顯示它吧!”。
4 <jsp:param>
還可以在<jsp:include>和<jsp:forward>標簽中使用<jsp:param>子標簽,它是用來傳遞參數的。下面用<jsp:include>來舉例說明<jsp:param>的使用。
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title>a.jsp</title> </head> <body> <h1>a.jsp</h1> <hr/> <jsp:include page="/b.jsp"> <jsp:param value="zhangSan" name="username"/> </jsp:include> </body> </html> |
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title>b.jsp</title> </head> <body> <h1>b.jsp</h1> <hr/> <% String username = request.getParameter("username"); out.print("你好:" + username); %> </body> </html> |
JavaBean(了解即可,非重要)
1 JavaBean概述
1.1 什么是JavaBean
JavaBean是一種規范,也就是對類的要求。它要求Java類的成員變量提供getter/setter方法,這樣的成員變量被稱之為JavaBean屬性。
JavaBean還要求類必須提供僅有的無參構造器,例如:public User() {…}
User.java
package cn.itcast.domain; public class User { private String username; private String password; 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; } } |
1.2 JavaBean屬性
JavaBean屬性是具有getter/setter方法的成員變量。
l 也可以只提供getter方法,這樣的屬性叫只讀屬性;
l 也可以只提供setter方法,這樣的屬性叫只寫屬性;
l 如果屬性類型為boolean類型,那么讀方法的格式可以是get或is。例如名為abc的boolean類型的屬性,它的讀方法可以是getAbc(),也可以是isAbc();
JavaBean屬性名要求:前兩個字母要么都大寫,要么都小寫:
public class User { private String iD; private String ID; private String qQ; private String QQ; … } |
JavaBean可能存在屬性,但不存在這個成員變量,例如:
public class User { public String getUsername() { return "zhangSan"; } } |
上例中User類有一個名為username的只讀屬性!但User類並沒有username這個成員變量!
還可以並變態一點:
public class User { private String hello; public String getUsername() { return hello; } public void setUsername(String username) { this.hello = username; } } |
上例中User類中有一個名為username的屬性,它是可讀可寫的屬性!而Use類的成員變量名為hello!也就是說JavaBean的屬性名取決與方法名稱,而不是成員變量的名稱。但通常沒有人做這么變態的事情。
2 內省
內省的目標是得到JavaBean屬性的讀、寫方法的反射對象,通過反射對JavaBean屬性進行操作的一組API。例如User類有名為username的JavaBean屬性,通過兩個Method對象(一個是getUsenrmae(),一個是setUsername())來操作User對象。
如果你還不能理解內省是什么,那么我們通過一個問題來了解內省的作用。現在我們有一個Map,內容如下:
Map<String,String> map = new HashMap<String,String>(); map.put("username", "admin"); map.put("password", "admin123"); |
public class User { private String username; private String password; public User(String username, String password) { this.username = username; this.password = password; } public User() { } 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 toString() { return "User [username=" + username + ", password=" + password + "]"; } } |
現在需要把map的數據封裝到一個User對象中!User類有兩個JavaBean屬性,一個叫username,另一個叫password。
你可能想到的是反射,通過map的key來查找User類的Field!這么做是沒有問題的,但我們要知道類的成員變量是私有的,雖然也可以通過反射去訪問類的私有的成員變量,但我們也要清楚反射訪問私有的東西是有“危險”的,所以還是建議通過getUsername和setUsername來訪問JavaBean屬性。
2.1 內省之獲取BeanInfo
我們這里不想去對JavaBean規范做過多的介紹,所以也就不在多介紹BeanInfo的“出身”了。你只需要知道如何得到它,以及BeanInfo有什么。
通過java.beans.Introspector的getBeanInfo()方法來獲取java.beans.BeanInfo實例。
BeanInfo beanInfo = Introspector.getBeanInfo(User.class); |
2.2 得到所有屬性描述符(PropertyDescriptor)
通過BeanInfo可以得到這個類的所有JavaBean屬性的PropertyDescriptor對象。然后就可以通過PropertyDescriptor對象得到這個屬性的getter/setter方法的Method對象了。
PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors(); |
每個PropertyDescriptor對象對應一個JavaBean屬性:
l String getName():獲取JavaBean屬性名稱;
l Method getReadMethod:獲取屬性的讀方法;
l Method getWriteMethod:獲取屬性的寫方法。
2.3 完成Map數據封裝到User對象中
public void fun1() throws Exception { Map<String,String> map = new HashMap<String,String>(); map.put("username", "admin"); map.put("password", "admin123"); BeanInfo beanInfo = Introspector.getBeanInfo(User.class); PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors(); User user = new User(); for(PropertyDescriptor pd : pds) { String name = pd.getName(); String value = map.get(name); if(value != null) { Method writeMethod = pd.getWriteMethod(); writeMethod.invoke(user, value); } } System.out.println(user); } |
3 commons-beanutils
提到內省,不能不提commons-beanutils這個工具。它底層使用了內省,對內省進行了大量的簡化!
使用beanutils需要的jar包:
l commons-beanutils.jar;
l commons-logging.jar;
3.1 設置JavaBean屬性
User user = new User(); BeanUtils.setProperty(user, "username", "admin"); BeanUtils.setProperty(user, "password", "admin123"); System.out.println(user); |
3.2 獲取JavaBean屬性
User user = new User("admin", "admin123"); String username = BeanUtils.getProperty(user, "username"); String password = BeanUtils.getProperty(user, "password"); System.out.println("username=" + username + ", password=" + password); |
3.3 封裝Map數據到JavaBean對象中
Map<String,String> map = new HashMap<String,String>(); map.put("username", "admin"); map.put("password", "admin123"); User user = new User(); BeanUtils.populate(user, map); System.out.println(user); |
4 JSP與JavaBean相關的動作標簽
在JSP中與JavaBean相關的標簽有:
l <jsp:useBean>:創建JavaBean對象;
l <jsp:setProperty>:設置JavaBean屬性;
l <jsp:getProperty>:獲取JavaBean屬性;
我們需要先創建一個JavaBean類:
User.java
package cn.itcast.domain; public class User { private String username; private String password; public User(String username, String password) { this.username = username; this.password = password; } public User() { } 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 toString() { return "User [username=" + username + ", password=" + password + "]"; } } |
4.1 <jsp:useBean>
<jsp:useBean>標簽的作用是創建JavaBean對象:
l 在當前JSP頁面創建JavaBean對象;
l 把創建的JavaBean對象保存到域對象中;
<jsp:useBean id="user1" class="cn.itcast.domain.User" /> |
上面代碼表示在當前JSP頁面中創建User類型的對象,並且把它保存到page域中了。下面我們把<jsp:useBean>標簽翻譯成Java代碼:
<% cn.itcast.domain.User user1 = new cn.itcast.domain.User(); pageContext.setAttribute("user1", user1); %> |
這說明我們可以在JSP頁面中完成下面的操作:
<jsp:useBean id="user1" class="cn.itcast.domain.User" /> <%=user1 %> <% out.println(pageContext.getAttribute("user1")); %> |
<jsp:useBean>標簽默認是把JavaBean對象保存到page域,還可以通過scope標簽屬性來指定保存的范圍:
<jsp:useBean id="user1" class="cn.itcast.domain.User" scope="page"/> <jsp:useBean id="user2" class="cn.itcast.domain.User" scope="request"/> <jsp:useBean id="user3" class="cn.itcast.domain.User" scope="session"/> <jsp:useBean id="user4" class="cn.itcast.domain.User" scope="applicatioin"/> |
<jsp:useBean>標簽其實不一定會創建對象!!!其實它會先在指定范圍中查找這個對象,如果對象不存在才會創建,我們需要重新對它進行翻譯:
<jsp:useBean id="user4" class="cn.itcast.domain.User" scope="applicatioin"/> |
<% cn.itcast.domain.User user4 = (cn.itcast.domain.User)application.getAttribute("user4"); if(user4 == null) { user4 = new cn.itcast.domain.User(); application.setAttribute("user4", user4); } %> |
4.2 <jsp:setProperty>和<jsp:getProperty>
<jsp:setProperty>標簽的作用是給JavaBean設置屬性值,而<jsp:getProperty>是用來獲取屬性值。在使用它們之前需要先創建JavaBean:
<jsp:useBean id="user1" class="cn.itcast.domain.User" /> <jsp:setProperty property="username" name="user1" value="admin"/> <jsp:setProperty property="password" name="user1" value="admin123"/> 用戶名:<jsp:getProperty property="username" name="user1"/><br/> 密 碼:<jsp:getProperty property="password" name="user1"/><br/> |
EL(表達式語言)
1 EL概述
1.1 EL的作用
JSP2.0要把html和css分離、要把html和javascript分離、要把Java腳本替換成標簽。標簽的好處是非Java人員都可以使用。
JSP2.0 – 純標簽頁面,即:不包含<% … %>、<%! … %>,以及<%= … %>
EL(Expression Language)是一門表達式語言,它對應<%=…%>。我們知道在JSP中,表達式會被輸出,所以EL表達式也會被輸出。
1.2 EL的格式
格式:${…}
例如:${1 + 2}
1.3 關閉EL
如果希望整個JSP忽略EL表達式,需要在page指令中指定isELIgnored=”true”。
如果希望忽略某個EL表達式,可以在EL表達式之前添加“\”,例如:\${1 + 2}。
1.4 EL運算符
運算符 |
說明 |
范例 |
結果 |
+ |
加 |
${17+5} |
22 |
- |
減 |
${17-5} |
12 |
* |
乘 |
${17*5} |
85 |
/或div |
除 |
${17/5}或${17 div 5} |
3 |
%或mod |
取余 |
${17%5}或${17 mod 5} |
2 |
==或eq |
等於 |
${5==5}或${5 eq 5} |
true |
!=或ne |
不等於 |
${5!=5}或${5 ne 5} |
false |
<或lt |
小於 |
${3<5}或${3 lt 5} |
true |
>或gt |
大於 |
${3>5}或${3 gt 5} |
false |
<=或le |
小於等於 |
${3<=5}或${3 le 5} |
true |
>=或ge |
大於等於 |
${3>=5}或${3 ge 5} |
false |
&&或and |
並且 |
${true&&false}或${true and false} |
false |
!或not |
非 |
${!true}或${not true} |
false |
||或or |
或者 |
${true||false}或${true or false} |
true |
empty |
是否為空 |
${empty “”},可以判斷字符串、數據、集合的長度是否為0,為0返回true。empty還可以與not或!一起使用。${not empty “”} |
true |
1.5 EL不顯示null
當EL表達式的值為null時,會在頁面上顯示空白,即什么都不顯示。
2 EL表達式格式
先來了解一下EL表達式的格式!現在還不能演示它,因為需要學習了EL11個內置對象后才方便顯示它。
l 操作List和數組:${list[0]}、${arr[0]};
l 操作bean的屬性:${person.name}、${person[‘name’]},對應person.getName()方法;
l 操作Map的值:${map.key}、${map[‘key’]},對應map.get(key)。
3 EL內置對象
EL一共11個內置對象,無需創建即可以使用。這11個內置對象中有10個是Map類型的,最后一個是pageContext對象。
l pageScope
l requestScope
l sessionScope
l applicationScope
l param;
l paramValues;
l header;
l headerValues;
l initParam;
l cookie;
l pageContext;
3.1 域相關內置對象(重點)
域內置對象一共有四個:
l pageScope:${pageScope.name}等同與pageContext.getAttribute(“name”);
l requestScope:${requestScope.name}等同與request.getAttribute(“name”);
l sessionScoep: ${sessionScope.name}等同與session.getAttribute(“name”);
l applicationScope:${applicationScope.name}等同與application.getAttribute(“name”);
如果在域中保存的是JavaBean對象,那么可以使用EL來訪問JavaBean屬性。因為EL只做讀取操作,所以JavaBean一定要提供get方法,而set方法沒有要求。
Person.java
public class Person { private String name; private int age; private String sex; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } } |
全域查找:${person}表示依次在pageScope、requesScopet、sessionScope、appliationScope四個域中查找名字為person的屬性。
3.2 請求參數相關內置對象
param和paramValues這兩個內置對象是用來獲取請求參數的。
l param:Map<String,String>類型,param對象可以用來獲取參數,與request.getParameter()方法相同。
注意,在使用EL獲取參數時,如果參數不存在,返回的是空字符串,而不是null。這一點與使用request.getParameter()方法是不同的。
l paramValues:paramValues是Map<String, String[]>類型,當一個參數名,對應多個參數值時可以使用它。
3.3 請求頭相關內置對象
header和headerValues是與請求頭相關的內置對象:
l header: Map<String,String>類型,用來獲取請求頭。
l headerValues:headerValues是Map<String,String[]>類型。當一個請求頭名稱,對應多個值時,使用該對象,這里就不在贅述。
3.4 應用初始化參數相關內置對象
l initParam:initParam是Map<String,String>類型。它對應web.xml文件中的<context-param>參數。
3.5 Cookie相關內置對象
l cookie:cookie是Map<String,Cookie>類型,其中key是Cookie的名字,而值是Cookie對象本身。
3.6 pageContext對象
pageContext:pageContext是PageContext類型!可以使用pageContext對象調用getXXX()方法,例如pageContext.getRequest(),可以${pageContext.request}。也就是讀取JavaBean屬性!!!
EL表達式 |
說明 |
${pageContext.request.queryString} |
pageContext.getRequest().getQueryString(); |
${pageContext.request.requestURL} |
pageContext.getRequest().getRequestURL(); |
${pageContext.request.contextPath} |
pageContext.getRequest().getContextPath(); |
${pageContext.request.method} |
pageContext.getRequest().getMethod(); |
${pageContext.request.protocol} |
pageContext.getRequest().getProtocol(); |
${pageContext.request.remoteUser} |
pageContext.getRequest().getRemoteUser(); |
${pageContext.request.remoteAddr} |
pageContext.getRequest().getRemoteAddr(); |
${pageContext.session.new} |
pageContext.getSession().isNew(); |
${pageContext.session.id} |
pageContext.getSession().getId(); |
${pageContext.servletContext.serverInfo} |
pageContext.getServletContext().getServerInfo(); |
EL函數庫
1 什么EL函數庫
EL函數庫是由第三方對EL的擴展,我們現在學習的EL函數庫是由JSTL添加的。JSTL明天再學!
EL函數庫就是定義一些有返回值的靜態方法。然后通過EL語言來調用它們!當然,不只是JSTL可以定義EL函數庫,我們也可以自定義EL函數庫。
EL函數庫中包含了很多對字符串的操作方法,以及對集合對象的操作。例如:${fn:length(“abc”)}會輸出3,即字符串的長度。
2 導入函數庫
因為是第三方的東西,所以需要導入。導入需要使用taglib指令!
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
3 EL函數庫介紹
l String toUpperCase(String input):
l String toLowerCase(String input):
l int indexOf(String input, String substring):
l boolean contains(String input, String substring):
l boolean containsIgnoreCase(String input, String substring):
l boolean startsWith(String input, String substring):
l boolean endsWith(String input, String substring):
l String substring(String input, int beginIndex, int endIndex):
l String substringAfter(String input, String substring):
l substringBefore(String input, String substring):
l String escapeXml(String input):”、’、<、>、&
l String trim(String input):
l String replace(String input, String substringBefore, String substringAfter):
l String[] split(String input, String delimiters):
l int length(Object obj):
l String join(String array[], String separator):
<%@taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> … String[] strs = {"a", "b","c"}; List list = new ArrayList(); list.add("a"); pageContext.setAttribute("arr", strs); pageContext.setAttribute("list", list); %> ${fn:length(arr) }<br/><!--3--> ${fn:length(list) }<br/><!--1--> ${fn:toLowerCase("Hello") }<br/> <!-- hello --> ${fn:toUpperCase("Hello") }<br/> <!-- HELLO --> ${fn:contains("abc", "a")}<br/><!-- true --> ${fn:containsIgnoreCase("abc", "Ab")}<br/><!-- true --> ${fn:contains(arr, "a")}<br/><!-- true --> ${fn:containsIgnoreCase(list, "A")}<br/><!-- true --> ${fn:endsWith("Hello.java", ".java")}<br/><!-- true --> ${fn:startsWith("Hello.java", "Hell")}<br/><!-- true --> ${fn:indexOf("Hello-World", "-")}<br/><!-- 5 --> ${fn:join(arr, ";")}<br/><!-- a;b;c --> ${fn:replace("Hello-World", "-", "+")}<br/><!-- Hello+World --> ${fn:join(fn:split("a;b;c;", ";"), "-")}<br/><!-- a-b-c --> ${fn:substring("0123456789", 6, 9)}<br/><!-- 678 --> ${fn:substring("0123456789", 5, -1)}<br/><!-- 56789 --> ${fn:substringAfter("Hello-World", "-")}<br/><!-- World --> ${fn:substringBefore("Hello-World", "-")}<br/><!-- Hello --> ${fn:trim(" a b c ")}<br/><!-- a b c --> ${fn:escapeXml("<html></html>")}<br/> <!-- <html></html> --> |
4 自定義EL函數庫
l 寫一個類,寫一個有返回值的靜態方法;
l 編寫itcast.tld文件,可以參數fn.tld文件來寫,把itcast.tld文件放到/WEB-INF目錄下;
l 在頁面中添加taglib指令,導入自定義標簽庫。
ItcastFuncations.java
package cn.itcast.el.funcations; public class ItcastFuncations { public static String test() { return "傳智播客自定義EL函數庫測試"; } } |
itcast.tld(放到classes下)
<?xml version="1.0" encoding="UTF-8" ?> <taglib xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd" version="2.0"> <tlib-version>1.0</tlib-version> <short-name>itcast</short-name> <uri>http://www.itcast.cn/jsp/functions</uri> <function> <name>test</name> <function-class>cn.itcast.el.funcations.ItcastFuncations</function-class> <function-signature>String test()</function-signature> </function> </taglib> |
index.jsp
MySQL
數據庫
1 數據庫概念(了解)
1.1 什么是數據庫
數據庫就是用來存儲和管理數據的倉庫!
數據庫存儲數據的優先:
l
可存儲大量數據;
l 方便檢索;
l 保持數據的一致性、完整性;
l 安全,可共享;
l 通過組合分析,可產生新數據。
1.2 數據庫的發展歷程
l 沒有數據庫,使用磁盤文件存儲數據;
l 層次結構模型數據庫;
l 網狀結構模型數據庫;
l 關系結構模型數據庫:使用二維表格來存儲數據;
l 關系-對象模型數據庫;
MySQL就是關系型數據庫!
1.3 常見數據庫
l Oracle:甲骨文;
l DB2:IBM;
l SQL Server:微軟;
l Sybase:賽爾斯;
l MySQL:甲骨文;
1.4 理解數據庫
我們現在所說的數據庫泛指關“系型數據庫管理系統(RDBMS - Relational database management system)”,即“數據庫服務器”。
當我們安裝了數據庫服務器后,就可以在數據庫服務器中創建數據庫,每個數據庫中還可以包含多張表。
數據庫表就是一個多行多列的表格。在創建表時,需要指定表的列數,以及列名稱,列類型等信息。而不用指定表格的行數,行數是沒有上限的。下面是tab_student表的結構:
當把表格創建好了之后,就可以向表格中添加數據了。向表格添加數據是以行為單位的!下面是s_student表的記錄:
s_id |
s_name |
s_age |
s_sex |
S_1001 |
zhangSan |
23 |
male |
S_1002 |
liSi |
32 |
female |
S_1003 |
wangWu |
44 |
male |
大家要學會區分什么是表結構,什么是表記錄。
1.5 應用程序與數據庫
應用程序使用數據庫完成對數據的存儲!
2 安裝MySQL數據庫
2.1 安裝MySQL
參考:MySQL安裝圖解.doc
2.2 MySQL目錄結構
MySQL的數據存儲目錄為data,data目錄通常在C:\Documents and Settings\All Users\Application Data\MySQL\MySQL Server 5.1\data位置。在data下的每個目錄都代表一個數據庫。
MySQL的安裝目錄下:
l bin目錄中都是可執行文件;
l my.ini文件是MySQL的配置文件;
3 基本命令
3.1 啟動和關閉mysql服務器
l 啟動:net start mysql;
l 關閉:net stop mysql;
在啟動mysql服務后,打開windows任務管理器,會有一個名為mysqld.exe的進程運行,所以mysqld.exe才是MySQL服務器程序。
3.2 客戶端登錄退出mysql
在啟動MySQL服務器后,我們需要使用管理員用戶登錄MySQL服務器,然后來對服務器進行操作。登錄MySQL需要使用MySQL的客戶端程序:mysql.exe
l 登錄:mysql -u root -p 123 -h localhost;
Ø -u:后面的root是用戶名,這里使用的是超級管理員root;
Ø -p:后面的123是密碼,這是在安裝MySQL時就已經指定的密碼;
Ø -h:后面給出的localhost是服務器主機名,它是可以省略的,例如:mysql -u root -p 123;
l 退出:quit或exit;
在登錄成功后,打開windows任務管理器,會有一個名為mysql.exe的進程運行,所以mysql.exe是客戶端程序。
SQL語句
SQL概述
1.1 什么是SQL
SQL(Structured Query Language)是“結構化查詢語言”,它是對關系型數據庫的操作語言。它可以應用到所有關系型數據庫中,例如:MySQL、Oracle、SQL Server等。SQ標准(ANSI/ISO)有:
l SQL-92:1992年發布的SQL語言標准;
l SQL:1999:1999年發布的SQL語言標簽;
l SQL:2003:2003年發布的SQL語言標簽;
這些標准就與JDK的版本一樣,在新的版本中總要有一些語法的變化。不同時期的數據庫對不同標准做了實現。
雖然SQL可以用在所有關系型數據庫中,但很多數據庫還都有標准之后的一些語法,我們可以稱之為“方言”。例如MySQL中的LIMIT語句就是MySQL獨有的方言,其它數據庫都不支持!當然,Oracle或SQL Server都有自己的方言。
1.2 語法要求
l SQL語句可以單行或多行書寫,以分號結尾;
l 可以用空格和縮進來來增強語句的可讀性;
l 關鍵字不區別大小寫,建議使用大寫;
2 分類
l DDL(Data Definition Language):數據定義語言,用來定義數據庫對象:庫、表、列等;
l DML(Data Manipulation Language):數據操作語言,用來定義數據庫記錄(數據);
l DCL(Data Control Language):數據控制語言,用來定義訪問權限和安全級別;
l DQL(Data Query Language):數據查詢語言,用來查詢記錄(數據)。
3 DDL
3.1 基本操作
l 查看所有數據庫名稱:SHOW DATABASES;
l 切換數據庫:USE mydb1,切換到mydb1數據庫;
3.2 操作數據庫
l 創建數據庫:CREATE DATABASE [IF NOT EXISTS] mydb1;
創建數據庫,例如:CREATE DATABASE mydb1,創建一個名為mydb1的數據庫。如果這個數據已經存在,那么會報錯。例如CREATE DATABASE IF NOT EXISTS mydb1,在名為mydb1的數據庫不存在時創建該庫,這樣可以避免報錯。
l 刪除數據庫:DROP DATABASE [IF EXISTS] mydb1;
刪除數據庫,例如:DROP DATABASE mydb1,刪除名為mydb1的數據庫。如果這個數據庫不存在,那么會報錯。DROP DATABASE IF EXISTS mydb1,就算mydb1不存在,也不會的報錯。
l 修改數據庫編碼:ALTER DATABASE mydb1 CHARACTER SET utf8
修改數據庫mydb1的編碼為utf8。注意,在MySQL中所有的UTF-8編碼都不能使用中間的“-”,即UTF-8要書寫為UTF8。
3.3 數據(列)類型
MySQL與Java一樣,也有數據類型。MySQL中數據類型主要應用在列上。
常用類型:
l int:整型
l double:浮點型,例如double(5,2)表示最多5位,其中必須有2位小數,即最大值為999.99;
l decimal:浮點型,在表示錢方面使用該類型,因為不會出現精度缺失問題;
l char:固定長度字符串類型;
l varchar:可變長度字符串類型;
l text:字符串類型;
l blob:字節類型;
l date:日期類型,格式為:yyyy-MM-dd;
l time:時間類型,格式為:hh:mm:ss
l timestamp:時間戳類型;
3.4 操作表
l 創建表:
CREATE TABLE 表名(
列名 列類型,
列名 列類型,
......
);
例如:
CREATE TABLE stu( sidCHAR(6), snameVARCHAR(20), ageINT, genderVARCHAR(10) ); |
再例如:
CREATE TABLE emp( eidCHAR(6), enameVARCHAR(50), ageINT, genderVARCHAR(6), birthdayDATE, hiredateDATE, salaryDECIMAL(7,2), resumeVARCHAR(1000) ); |
l 查看當前數據庫中所有表名稱:SHOW TABLES;
l 查看指定表的創建語句:SHOW CREATE TABLE emp,查看emp表的創建語句;
l 查看表結構:DESC emp,查看emp表結構;
l 刪除表:DROP TABLE emp,刪除emp表;
l 修改表:
1. 修改之添加列:給stu表添加classname列:
ALTER TABLE stu ADD (classname varchar(100));
2. 修改之修改列類型:修改stu表的gender列類型為CHAR(2):
ALTER TABLE stu MODIFY gender CHAR(2);
3. 修改之修改列名:修改stu表的gender列名為sex:
ALTER TABLE stu change gender sex CHAR(2);
4. 修改之刪除列:刪除stu表的classname列:
ALTER TABLE stu DROP classname;
5. 修改之修改表名稱:修改stu表名稱為student:
ALTER TABLE stu RENAME TO student;
4 DML
4.1 插入數據
語法:
INSERT INTO 表名(列名1,列名2, …) VALUES(值1, 值2)
INSERT INTO stu(sid, sname,age,gender) VALUES('s_1001', 'zhangSan', 23, 'male'); |
INSERT INTO stu(sid, sname) VALUES('s_1001', 'zhangSan'); |
語法:
INSERT INTO 表名 VALUES(值1,值2,…)
因為沒有指定要插入的列,表示按創建表時列的順序插入所有列的值:
INSERT INTO stu VALUES('s_1002', 'liSi', 32, 'female'); |
注意:所有字符串數據必須使用單引用!
4.2 修改數據
語法:
UPDATE 表名 SET 列名1=值1, … 列名n=值n [WHERE 條件]
UPDATE stu SET sname=’zhangSanSan’, age=’32’, gender=’female’ WHERE sid=’s_1001’; |
UPDATE stu SET sname=’liSi’, age=’20’ WHERE age>50 AND gender=’male’; |
UPDATE stu SET sname=’wangWu’, age=’30’ WHERE age>60 OR gender=’female’; |
UPDATE stu SET gender=’female’ WHERE gender IS NULL UPDATE stu SET age=age+1 WHERE sname=’zhaoLiu’; |
4.3 刪除數據
語法:
DELETE FROM 表名 [WHERE 條件]
DELETE FROM stu WHERE sid=’s_1001’003B |
DELETE FROM stu WHERE sname=’chenQi’ OR age > 30; |
DELETE FROM stu; |
語法:
TRUNCATE TABLE 表名
TRUNCATE TABLE stu; |
雖然TRUNCATE和DELETE都可以刪除表的所有記錄,但有原理不同。DELETE的效率沒有TRUNCATE高!
TRUNCATE其實屬性DDL語句,因為它是先DROP TABLE,再CREATE TABLE。而且TRUNCATE刪除的記錄是無法回滾的,但DELETE刪除的記錄是可以回滾的(回滾是事務的知識!)。
5 DCL
5.1 創建用戶
語法:
CREATE USER 用戶名@地址 IDENTIFIED BY '密碼';
CREATE USER user1@localhost IDENTIFIED BY ‘123’; |
CREATE USER user2@’%’ IDENTIFIED BY ‘123’; |
5.2 給用戶授權
語法:
GRANT 權限1, … , 權限n ON 數據庫.* TO 用戶名
GRANT CREATE,ALTER,DROP,INSERT,UPDATE,DELETE,SELECT ON mydb1.* TO user1@localhost; |
GRANT ALL ON mydb1.* TO user2@localhost; |
5.3 撤銷授權
語法:
REVOKE權限1, … , 權限n ON 數據庫.* FORM 用戶名
REVOKE CREATE,ALTER,DROP ON mydb1.* FROM user1@localhost; |
5.4 查看用戶權限
語法:
SHOW GRANTS FOR 用戶名
SHOW GRANTS FOR user1@localhost; |
5.5 刪除用戶
語法:
DROP USER 用戶名
DROP USER user1@localhost; |
5.6 修改用戶密碼
語法:
USE mysql;
UPDATE USER SET PASSWORD=PASSWORD(‘密碼’) WHERE User=’用戶名’ and Host=’IP’;
FLUSH PRIVILEGES;
UPDATE USER SET PASSWORD=PASSWORD('1234') WHERE User='user2' and Host=’localhost’; FLUSH PRIVILEGES; |
數據查詢語法(DQL)
DQL就是數據查詢語言,數據庫執行DQL語句不會對數據進行改變,而是讓數據庫發送結果集給客戶端。
語法:
SELECT selection_list /*要查詢的列名稱*/
FROM table_list /*要查詢的表名稱*/
WHERE condition /*行條件*/
GROUP BY grouping_columns /*對結果分組*/
HAVING condition /*分組后的行條件*/
ORDER BY sorting_columns /*對結果分組*/
LIMIT offset_start, row_count /*結果限定*/
創建名:
l 學生表:stu
字段名稱 |
字段類型 |
說明 |
sid |
char(6) |
學生學號 |
sname |
varchar(50) |
學生姓名 |
age |
int |
學生年齡 |
gender |
varchar(50) |
學生性別 |
CREATE TABLE stu ( sid CHAR(6), sname VARCHAR(50), age INT, gender VARCHAR(50) ); |
INSERT INTO stu VALUES('S_1001', 'liuYi', 35, 'male'); INSERT INTO stu VALUES('S_1002', 'chenEr', 15, 'female'); INSERT INTO stu VALUES('S_1003', 'zhangSan', 95, 'male'); INSERT INTO stu VALUES('S_1004', 'liSi', 65, 'female'); INSERT INTO stu VALUES('S_1005', 'wangWu', 55, 'male'); INSERT INTO stu VALUES('S_1006', 'zhaoLiu', 75, 'female'); INSERT INTO stu VALUES('S_1007', 'sunQi', 25, 'male'); INSERT INTO stu VALUES('S_1008', 'zhouBa', 45, 'female'); INSERT INTO stu VALUES('S_1009', 'wuJiu', 85, 'male'); INSERT INTO stu VALUES('S_1010', 'zhengShi', 5, 'female'); INSERT INTO stu VALUES('S_1011', 'xxx', NULL, NULL); |
l 雇員表:emp
字段名稱 |
字段類型 |
說明 |
empno |
int |
員工編號 |
ename |
varchar(50) |
員工姓名 |
job |
varchar(50) |
員工工作 |
mgr |
int |
領導編號 |
hiredate |
date |
入職日期 |
sal |
decimal(7,2) |
月薪 |
comm |
decimal(7,2) |
獎金 |
deptno |
int |
部分編號 |
CREATE TABLE emp( empno INT, ename VARCHAR(50), job VARCHAR(50), mgr INT, hiredate DATE, sal DECIMAL(7,2), comm decimal(7,2), deptno INT ) ; |
INSERT INTO emp values(7369,'SMITH','CLERK',7902,'1980-12-17',800,NULL,20); INSERT INTO emp values(7499,'ALLEN','SALESMAN',7698,'1981-02-20',1600,300,30); INSERT INTO emp values(7521,'WARD','SALESMAN',7698,'1981-02-22',1250,500,30); INSERT INTO emp values(7566,'JONES','MANAGER',7839,'1981-04-02',2975,NULL,20); INSERT INTO emp values(7654,'MARTIN','SALESMAN',7698,'1981-09-28',1250,1400,30); INSERT INTO emp values(7698,'BLAKE','MANAGER',7839,'1981-05-01',2850,NULL,30); INSERT INTO emp values(7782,'CLARK','MANAGER',7839,'1981-06-09',2450,NULL,10); INSERT INTO emp values(7788,'SCOTT','ANALYST',7566,'1987-04-19',3000,NULL,20); INSERT INTO emp values(7839,'KING','PRESIDENT',NULL,'1981-11-17',5000,NULL,10); INSERT INTO emp values(7844,'TURNER','SALESMAN',7698,'1981-09-08',1500,0,30); INSERT INTO emp values(7876,'ADAMS','CLERK',7788,'1987-05-23',1100,NULL,20); INSERT INTO emp values(7900,'JAMES','CLERK',7698,'1981-12-03',950,NULL,30); INSERT INTO emp values(7902,'FORD','ANALYST',7566,'1981-12-03',3000,NULL,20); INSERT INTO emp values(7934,'MILLER','CLERK',7782,'1982-01-23',1300,NULL,10); |
l 部分表:dept
字段名稱 |
字段類型 |
說明 |
deptno |
int |
部分編碼 |
dname |
varchar(50) |
部分名稱 |
loc |
varchar(50) |
部分所在地點 |
CREATE TABLE dept( deptno INT, dname varchar(14), loc varchar(13) ); |
INSERT INTO dept values(10, 'ACCOUNTING', 'NEW YORK'); INSERT INTO dept values(20, 'RESEARCH', 'DALLAS'); INSERT INTO dept values(30, 'SALES', 'CHICAGO'); INSERT INTO dept values(40, 'OPERATIONS', 'BOSTON'); |
1 基礎查詢
1.1 查詢所有列
SELECT * FROM stu;
1.2 查詢指定列
SELECT sid, sname, age FROM stu;
2 條件查詢
2.1 條件查詢介紹
條件查詢就是在查詢時給出WHERE子句,在WHERE子句中可以使用如下運算符及關鍵字:
l =、!=、<>、<、<=、>、>=;
l BETWEEN…AND;
l IN(set);
l IS NULL;
l AND;
l OR;
l NOT;
2.2 查詢性別為女,並且年齡50的記錄
SELECT * FROM stu
WHERE gender='female' AND ge<50;
2.3 查詢學號為S_1001,或者姓名為liSi的記錄
SELECT * FROM stu
WHERE sid ='S_1001' OR sname='liSi';
2.4 查詢學號為S_1001,S_1002,S_1003的記錄
SELECT * FROM stu
WHERE sid IN ('S_1001','S_1002','S_1003');
2.5 查詢學號不是S_1001,S_1002,S_1003的記錄
SELECT * FROM tab_student
WHERE s_number NOT IN ('S_1001','S_1002','S_1003');
2.6 查詢年齡為null的記錄
SELECT * FROM stu
WHERE age IS NULL;
2.7 查詢年齡在20到40之間的學生記錄
SELECT *
FROM stu
WHERE age>=20 AND age<=40;
或者
SELECT *
FROM stu
WHERE age BETWEEN 20 AND 40;
2.8 查詢性別非男的學生記錄
SELECT *
FROM stu
WHERE gender!='male';
或者
SELECT *
FROM stu
WHERE gender<>'male';
或者
SELECT *
FROM stu
WHERE NOT gender='male';
2.9 查詢姓名不為null的學生記錄
SELECT *
FROM stu
WHERE NOT sname IS NULL;
或者
SELECT *
FROM stu
WHERE sname IS NOT NULL;
3 模糊查詢
當想查詢姓名中包含a字母的學生時就需要使用模糊查詢了。模糊查詢需要使用關鍵字LIKE。
3.1 查詢姓名由5個字母構成的學生記錄
SELECT *
FROM stu
WHERE sname LIKE '_____';
模糊查詢必須使用LIKE關鍵字。其中 “_”匹配任意一個字母,5個“_”表示5個任意字母。
3.2 查詢姓名由5個字母構成,並且第5個字母為“i”的學生記錄
SELECT *
FROM stu
WHERE sname LIKE '____i';
3.3 查詢姓名以“z”開頭的學生記錄
SELECT *
FROM stu
WHERE sname LIKE 'z%';
其中“%”匹配0~n個任何字母。
3.4 查詢姓名中第2個字母為“i”的學生記錄
SELECT *
FROM stu
WHERE sname LIKE '_i%';
3.5 查詢姓名中包含“a”字母的學生記錄
SELECT *
FROM stu
WHERE sname LIKE '%a%';
4 字段控制查詢
4.1 去除重復記錄
去除重復記錄(兩行或兩行以上記錄中系列的上的數據都相同),例如emp表中sal字段就存在相同的記錄。當只查詢emp表的sal字段時,那么會出現重復記錄,那么想去除重復記錄,需要使用DISTINCT:
SELECT DISTINCT sal FROM emp;
4.2 查看雇員的月薪與佣金之和
因為sal和comm兩列的類型都是數值類型,所以可以做加運算。如果sal或comm中有一個字段不是數值類型,那么會出錯。
SELECT *,sal+comm FROM emp;
comm列有很多記錄的值為NULL,因為任何東西與NULL相加結果還是NULL,所以結算結果可能會出現NULL。下面使用了把NULL轉換成數值0的函數IFNULL:
SELECT *,sal+IFNULL(comm,0) FROM emp;
4.3 給列名添加別名
在上面查詢中出現列名為sal+IFNULL(comm,0),這很不美觀,現在我們給這一列給出一個別名,為total:
SELECT *, sal+IFNULL(comm,0) AS total FROM emp;
給列起別名時,是可以省略AS關鍵字的:
SELECT *,sal+IFNULL(comm,0) total FROM emp;
5 排序
5.1 查詢所有學生記錄,按年齡升序排序
SELECT *
FROM stu
ORDER BY sage ASC;
或者
SELECT *
FROM stu
ORDER BY sage;
5.2 查詢所有學生記錄,按年齡降序排序
SELECT *
FROM stu
ORDER BY age DESC;
5.3 查詢所有雇員,按月薪降序排序,如果月薪相同時,按編號升序排序
SELECT * FROM emp
ORDER BY sal DESC,empno ASC;
6 聚合函數
聚合函數是用來做縱向運算的函數:
l COUNT():統計指定列不為NULL的記錄行數;
l MAX():計算指定列的最大值,如果指定列是字符串類型,那么使用字符串排序運算;
l MIN():計算指定列的最小值,如果指定列是字符串類型,那么使用字符串排序運算;
l SUM():計算指定列的數值和,如果指定列類型不是數值類型,那么計算結果為0;
l AVG():計算指定列的平均值,如果指定列類型不是數值類型,那么計算結果為0;
6.1 COUNT
當需要縱向統計時可以使用COUNT()。
l 查詢emp表中記錄數:
SELECT COUNT(*) AS cnt FROM emp;
l 查詢emp表中有佣金的人數:
SELECT COUNT(comm) cnt FROM emp;
注意,因為count()函數中給出的是comm列,那么只統計comm列非NULL的行數。
l 查詢emp表中月薪大於2500的人數:
SELECT COUNT(*) FROM emp
WHERE sal > 2500;
l 統計月薪與佣金之和大於2500元的人數:
SELECT COUNT(*) AS cnt FROM emp WHERE sal+IFNULL(comm,0) > 2500;
l 查詢有佣金的人數,以及有領導的人數:
SELECT COUNT(comm), COUNT(mgr) FROM emp;
6.2 SUM和AVG
當需要縱向求和時使用sum()函數。
l 查詢所有雇員月薪和:
SELECT SUM(sal) FROM emp;
l 查詢所有雇員月薪和,以及所有雇員佣金和:
SELECT SUM(sal), SUM(comm) FROM emp;
l 查詢所有雇員月薪+佣金和:
SELECT SUM(sal+IFNULL(comm,0)) FROM emp;
l 統計所有員工平均工資:
SELECT SUM(sal), COUNT(sal) FROM emp;
或者
SELECT AVG(sal) FROM emp;
6.3 MAX和MIN
l 查詢最高工資和最低工資:
SELECT MAX(sal), MIN(sal) FROM emp;
7 分組查詢
當需要分組查詢時需要使用GROUP BY子句,例如查詢每個部門的工資和,這說明要使用部分來分組。
7.1 分組查詢
l 查詢每個部門的部門編號和每個部門的工資和:
SELECT deptno, SUM(sal)
FROM emp
GROUP BY deptno;
l 查詢每個部門的部門編號以及每個部門的人數:
SELECT deptno,COUNT(*)
FROM emp
GROUP BY deptno;
l 查詢每個部門的部門編號以及每個部門工資大於1500的人數:
SELECT deptno,COUNT(*)
FROM emp
WHERE sal>1500
GROUP BY deptno;
7.2 HAVING子句
l 查詢工資總和大於9000的部門編號以及工資和:
SELECT deptno, SUM(sal)
FROM emp
GROUP BY deptno
HAVING SUM(sal) > 9000;
注意,WHERE是對分組前記錄的條件,如果某行記錄沒有滿足WHERE子句的條件,那么這行記錄不會參加分組;而HAVING是對分組后數據的約束。
8 LIMIT
LIMIT用來限定查詢結果的起始行,以及總行數。
8.1 查詢5行記錄,起始行從0開始
SELECT * FROM emp LIMIT 0, 5;
注意,起始行從0開始,即第一行開始!
8.2 查詢10行記錄,起始行從3開始
SELECT * FROM emp LIMIT 3, 10;
8.3 分頁查詢
如果一頁記錄為10條,希望查看第3頁記錄應該怎么查呢?
l 第一頁記錄起始行為0,一共查詢10行;
l 第二頁記錄起始行為10,一共查詢10行;
l 第三頁記錄起始行為20,一共查詢10行;
完整性約束
完整性約束是為了表的數據的正確性!如果數據不正確,那么一開始就不能添加到表中。
1 主鍵
當某一列添加了主鍵約束后,那么這一列的數據就不能重復出現。這樣每行記錄中其主鍵列的值就是這一行的唯一標識。例如學生的學號可以用來做唯一標識,而學生的姓名是不能做唯一標識的,因為學習有可能同名。
主鍵列的值不能為NULL,也不能重復!
指定主鍵約束使用PRIMARY KEY關鍵字
l 創建表:定義列時指定主鍵:
CREATE TABLE stu(
sidCHAR(6) PRIMARY KEY,
snameVARCHAR(20),
ageINT,
genderVARCHAR(10)
);
l 創建表:定義列之后獨立指定主鍵:
CREATE TABLE stu(
sidCHAR(6),
snameVARCHAR(20),
ageINT,
genderVARCHAR(10),
PRIMARY KEY(sid)
);
l 修改表時指定主鍵:
ALTER TABLE stu
ADD PRIMARY KEY(sid);
l 刪除主鍵(只是刪除主鍵約束,而不會刪除主鍵列):
ALTER TABLE stu DROP PRIMARY KEY;
2 主鍵自增長
MySQL提供了主鍵自動增長的功能!這樣用戶就不用再為是否有主鍵是否重復而煩惱了。當主鍵設置為自動增長后,在沒有給出主鍵值時,主鍵的值會自動生成,而且是最大主鍵值+1,也就不會出現重復主鍵的可能了。
l 創建表時設置主鍵自增長(主鍵必須是整型才可以自增長):
CREATE TABLE stu(
sid INT PRIMARY KEY AUTO_INCREMENT,
snameVARCHAR(20),
ageINT,
genderVARCHAR(10)
);
l 修改表時設置主鍵自增長:
ALTER TABLE stu CHANGE sid sid INT AUTO_INCREMENT;
l 修改表時刪除主鍵自增長:
ALTER TABLE stu CHANGE sid sid INT;
3 非空
指定非空約束的列不能沒有值,也就是說在插入記錄時,對添加了非空約束的列一定要給值;在修改記錄時,不能把非空列的值設置為NULL。
l 指定非空約束:
CREATE TABLE stu(
sid INT PRIMARY KEY AUTO_INCREMENT,
sname VARCHAR(10) NOT NULL,
ageINT,
genderVARCHAR(10)
);
當為sname字段指定為非空后,在向stu表中插入記錄時,必須給sname字段指定值,否則會報錯:
INSERT INTO stu(sid) VALUES(1);
插入的記錄中sname沒有指定值,所以會報錯!
4 唯一
還可以為字段指定唯一約束!當為字段指定唯一約束后,那么字段的值必須是唯一的。這一點與主鍵相似!例如給stu表的sname字段指定唯一約束:
CREATE TABLE tab_ab(
sid INT PRIMARY KEY AUTO_INCREMENT,
sname VARCHAR(10) UNIQUE
);
INSERT INTO sname(sid, sname) VALUES(1001, 'zs');
INSERT INTO sname(sid, sname) VALUES(1002, 'zs');
當兩次插入相同的名字時,MySQL會報錯!
5 外鍵
主外鍵是構成表與表關聯的唯一途徑!
外鍵是另一張表的主鍵!例如員工表與部門表之間就存在關聯關系,其中員工表中的部門編號字段就是外鍵,是相對部門表的外鍵。
我們再來看BBS系統中:用戶表(t_user)、分類表(t_section)、帖子表(t_topic)三者之間的關系。
例如在t_section表中sid為1的記錄說明有一個分類叫java,版主是t_user表中uid為1的用戶,即zs!
例如在t_topic表中tid為2的記錄是名字為“Java是咖啡”的帖子,它是java版塊的帖子,它的作者是ww。
外鍵就是用來約束這一列的值必須是另一張表的主鍵值!!!
l 創建t_user表,指定uid為主鍵列:
CREATE TABLE t_user(
uidINT PRIMARY KEY AUTO_INCREMENT,
unameVARCHAR(20) UNIQUE NOT NULL
);
l 創建t_section表,指定sid為主鍵列,u_id為相對t_user表的uid列的外鍵:
CREATE TABLE t_section(
sidINT PRIMARY KEY AUTO_INCREMENT,
snameVARCHAR(30),
u_idINT,
CONSTRAINT fk_t_user FOREIGN KEY(u_id) REFERENCES t_user(uid)
);
l 修改t_section表,指定u_id為相對t_user表的uid列的外鍵:
ALTER TABLE t_section
ADD CONSTRAINT fk_t_user
FOREIGN KEY(u_id)
REFERENCES t_user(uid);
l 修改t_section表,刪除u_id的外鍵約束:
ALTER TABLE t_section
DROP FOREIGN KEY fk_t_user;
6 表與表之間的關系
l 一對一:例如t_person表和t_card表,即人和身份證。這種情況需要找出主從關系,即誰是主表,誰是從表。人可以沒有身份證,但身份證必須要有人才行,所以人是主表,而身份證是從表。設計從表可以有兩種方案:
Ø 在t_card表中添加外鍵列(相對t_user表),並且給外鍵添加唯一約束;
Ø 給t_card表的主鍵添加外鍵約束(相對t_user表),即t_card表的主鍵也是外鍵。
l 一對多(多對一):最為常見的就是一對多!一對多和多對一,這是從哪個角度去看得出來的。t_user和t_section的關系,從t_user來看就是一對多,而從t_section的角度來看就是多對一!這種情況都是在多方創建外鍵!
l 多對多:例如t_stu和t_teacher表,即一個學生可以有多個老師,而一個老師也可以有多個學生。這種情況通常需要創建中間表來處理多對多關系。例如再創建一張表t_stu_tea表,給出兩個外鍵,一個相對t_stu表的外鍵,另一個相對t_teacher表的外鍵。
編碼
1 查看MySQL編碼
SHOW VARIABLES LIKE 'char%';
因為當初安裝時指定了字符集為UTF8,所以所有的編碼都是UTF8。
l character_set_client:你發送的數據必須與client指定的編碼一致!!!服務器會使用該編碼來解讀客戶端發送過來的數據;
l character_set_connection:通過該編碼與client一致!該編碼不會導致亂碼!當執行的是查詢語句時,客戶端發送過來的數據會先轉換成connection指定的編碼。但只要客戶端發送過來的數據與client指定的編碼一致,那么轉換就不會出現問題;
l character_set_database:數據庫默認編碼,在創建數據庫時,如果沒有指定編碼,那么默認使用database編碼;
l character_set_server:MySQL服務器默認編碼;
l character_set_results:響應的編碼,即查詢結果返回給客戶端的編碼。這說明客戶端必須使用result指定的編碼來解碼;
2 控制台編碼
修改character_set_client、character_set_results、character_set_connection為GBK,就不會出現亂碼了。但其實只需要修改character_set_client和character_set_results。
控制台的編碼只能是GBK,而不能修改為UTF8,這就出現一個問題。客戶端發送的數據是GBK,而character_set_client為UTF8,這就說明客戶端數據到了服務器端后一定會出現亂碼。既然不能修改控制台的編碼,那么只能修改character_set_client為GBK了。
服務器發送給客戶端的數據編碼為character_set_result,它如果是UTF8,那么控制台使用GBK解碼也一定會出現亂碼。因為無法修改控制台編碼,所以只能把character_set_result修改為GBK。
l 修改character_set_client變量:set character_set_client=gbk;
l 修改character_set_results變量:set character_set_results=gbk;
設置編碼只對當前連接有效,這說明每次登錄MySQL提示符后都要去修改這兩個編碼,但可以通過修改配置文件來處理這一問題:配置文件路徑:D:\Program Files\MySQL\MySQL Server 5.1\ my.ini
3 MySQL工具
使用MySQL工具是不會出現亂碼的,因為它們會每次連接時都修改character_set_client、character_set_results、character_set_connection的編碼。這樣對my.ini上的配置覆蓋了,也就不會出現亂碼了。
MySQL數據庫備份與還原
備份和恢復數據
1 生成SQL腳本
在控制台使用mysqldump命令可以用來生成指定數據庫的腳本文本,但要注意,腳本文本中只包含數據庫的內容,而不會存在創建數據庫的語句!所以在恢復數據時,還需要自已手動創建一個數據庫之后再去恢復數據。
mysqldump –u用戶名 –p密碼 數據庫名>生成的腳本文件路徑 |
現在可以在C盤下找到mydb1.sql文件了!
注意,mysqldump命令是在Windows控制台下執行,無需登錄mysql!!!
2 執行SQL腳本
執行SQL腳本需要登錄mysql,然后進入指定數據庫,才可以執行SQL腳本!!!
執行SQL腳本不只是用來恢復數據庫,也可以在平時編寫SQL腳本,然后使用執行SQL 腳本來操作數據庫!大家都知道,在黑屏下編寫SQL語句時,就算發現了錯誤,可能也不能修改了。所以我建議大家使用腳本文件來編寫SQL代碼,然后執行之!
SOURCE C:\mydb1.sql |
注意,在執行腳本時需要先行核查當前數據庫中的表是否與腳本文件中的語句有沖突!例如在腳本文件中存在create table a的語句,而當前數據庫中已經存在了a表,那么就會出錯!
還可以通過下面的方式來執行腳本文件:
mysql -uroot -p123 mydb1<c:\mydb1.sql
mysql –u用戶名 –p密碼 數據庫<要執行腳本文件路徑 |
這種方式無需登錄mysql!
多表查詢
多表查詢有如下幾種:
l 合並結果集;
l 連接查詢
Ø 內連接
Ø 外連接
² 左外連接
² 右外連接
² 全外連接(MySQL不支持)
Ø 自然連接
l 子查詢
1 合並結果集
1. 作用:合並結果集就是把兩個select語句的查詢結果合並到一起!
2. 合並結果集有兩種方式:
l UNION:去除重復記錄,例如:SELECT * FROM t1 UNION SELECT * FROM t2;
l UNION ALL:不去除重復記錄,例如:SELECT * FROM t1 UNION ALL SELECT * FROM t2。
3. 要求:被合並的兩個結果:列數、列類型必須相同。
2 連接查詢
連接查詢就是求出多個表的乘積,例如t1連接t2,那么查詢出的結果就是t1*t2。
連接查詢會產生笛卡爾積,假設集合A={a,b},集合B={0,1,2},則兩個集合的笛卡爾積為{(a,0),(a,1),(a,2),(b,0),(b,1),(b,2)}。可以擴展到多個集合的情況。
那么多表查詢產生這樣的結果並不是我們想要的,那么怎么去除重復的,不想要的記錄呢,當然是通過條件過濾。通常要查詢的多個表之間都存在關聯關系,那么就通過關聯關系去除笛卡爾積。
你能想像到emp和dept表連接查詢的結果么?emp一共14行記錄,dept表一共4行記錄,那么連接后查詢出的結果是56行記錄。
也就你只是想在查詢emp表的同時,把每個員工的所在部門信息顯示出來,那么就需要使用主外鍵來去除無用信息了。
使用主外鍵關系做為條件來去除無用信息
SELECT * FROM emp,dept WHERE emp.deptno=dept.deptno; |
上面查詢結果會把兩張表的所有列都查詢出來,也許你不需要那么多列,這時就可以指定要查詢的列了。
SELECT emp.ename,emp.sal,emp.comm,dept.dname FROM emp,dept WHERE emp.deptno=dept.deptno; |
還可以為表指定別名,然后在引用列時使用別名即可。
SELECT e.ename,e.sal,e.comm,d.dname FROM emp AS e,dept AS d WHERE e.deptno=d.deptno; |
2.1 內連接
上面的連接語句就是內連接,但它不是SQL標准中的查詢方式,可以理解為方言!SQL標准的內連接為:
SELECT * FROM emp e INNER JOIN dept d ON e.deptno=d.deptno; |
內連接的特點:查詢結果必須滿足條件。例如我們向emp表中插入一條記錄:
其中deptno為50,而在dept表中只有10、20、30、40部門,那么上面的查詢結果中就不會出現“張三”這條記錄,因為它不能滿足e.deptno=d.deptno這個條件。
2.2 外連接(左連接、右連接)
外連接的特點:查詢出的結果存在不滿足條件的可能。
左連接:
SELECT * FROM emp e LEFT OUTER JOIN dept d ON e.deptno=d.deptno; |
左連接是先查詢出左表(即以左表為主),然后查詢右表,右表中滿足條件的顯示出來,不滿足條件的顯示NULL。
這么說你可能不太明白,我們還是用上面的例子來說明。其中emp表中“張三”這條記錄中,部門編號為50,而dept表中不存在部門編號為50的記錄,所以“張三”這條記錄,不能滿足e.deptno=d.deptno這條件。但在左連接中,因為emp表是左表,所以左表中的記錄都會查詢出來,即“張三”這條記錄也會查出,但相應的右表部分顯示NULL。
2.3 右連接
右連接就是先把右表中所有記錄都查詢出來,然后左表滿足條件的顯示,不滿足顯示NULL。例如在dept表中的40部門並不存在員工,但在右連接中,如果dept表為右表,那么還是會查出40部門,但相應的員工信息為NULL。
SELECT * FROM emp e RIGHT OUTER JOIN dept d ON e.deptno=d.deptno; |
連接查詢心得:
連接不限與兩張表,連接查詢也可以是三張、四張,甚至N張表的連接查詢。通常連接查詢不可能需要整個笛卡爾積,而只是需要其中一部分,那么這時就需要使用條件來去除不需要的記錄。這個條件大多數情況下都是使用主外鍵關系去除。
兩張表的連接查詢一定有一個主外鍵關系,三張表的連接查詢就一定有兩個主外鍵關系,所以在大家不是很熟悉連接查詢時,首先要學會去除無用笛卡爾積,那么就是用主外鍵關系作為條件來處理。如果兩張表的查詢,那么至少有一個主外鍵條件,三張表連接至少有兩個主外鍵條件。
3 自然連接
大家也都知道,連接查詢會產生無用笛卡爾積,我們通常使用主外鍵關系等式來去除它。而自然連接無需你去給出主外鍵等式,它會自動找到這一等式:
l 兩張連接的表中名稱和類型完成一致的列作為條件,例如emp和dept表都存在deptno列,並且類型一致,所以會被自然連接找到!
當然自然連接還有其他的查找條件的方式,但其他方式都可能存在問題!
SELECT * FROM emp NATURAL JOIN dept; SELECT * FROM emp NATURAL LEFT JOIN dept; SELECT * FROM emp NATURAL RIGHT JOIN dept; |
4 子查詢
子查詢就是嵌套查詢,即SELECT中包含SELECT,如果一條語句中存在兩個,或兩個以上SELECT,那么就是子查詢語句了。
l 子查詢出現的位置:
Ø where后,作為條件的一部分;
Ø from后,作為被查詢的一條表;
l 當子查詢出現在where后作為條件時,還可以使用如下關鍵字:
Ø any
Ø all
l 子查詢結果集的形式:
Ø 單行單列(用於條件)
Ø 單行多列(用於條件)
Ø 多行單列(用於條件)
Ø 多行多列(用於表)
練習:
1. 工資高於甘寧的員工。
分析:
查詢條件:工資>甘寧工資,其中甘寧工資需要一條子查詢。
第一步:查詢甘寧的工資
SELECT sal FROM emp WHERE ename='甘寧' |
第二步:查詢高於甘寧工資的員工
SELECT * FROM emp WHERE sal > (${第一步}) |
結果:
SELECT * FROM emp WHERE sal > (SELECT sal FROM emp WHERE ename='甘寧') |
l 子查詢作為條件
l 子查詢形式為單行單列
2. 工資高於30部門所有人的員工信息
分析:
查詢條件:工資高於30部門所有人工資,其中30部門所有人工資是子查詢。高於所有需要使用all關鍵字。
第一步:查詢30部門所有人工資
SELECT sal FROM emp WHERE deptno=30; |
第二步:查詢高於30部門所有人工資的員工信息
SELECT * FROM emp WHERE sal > ALL (${第一步}) |
結果:
SELECT * FROM emp WHERE sal > ALL (SELECT sal FROM emp WHERE deptno=30) |
l 子查詢作為條件
l 子查詢形式為多行單列(當子查詢結果集形式為多行單列時可以使用ALL或ANY關鍵字)
3. 查詢工作和工資與殷天正完全相同的員工信息
分析:
查詢條件:工作和工資與殷天正完全相同,這是子查詢
第一步:查詢出殷天正的工作和工資
SELECT job,sal FROM emp WHERE ename='殷天正' |
第二步:查詢出與殷天正工作和工資相同的人
SELECT * FROM emp WHERE (job,sal) IN (${第一步}) |
結果:
SELECT * FROM emp WHERE (job,sal) IN (SELECT job,sal FROM emp WHERE ename='殷天正') |
l 子查詢作為條件
l 子查詢形式為單行多列
4. 查詢員工編號為1006的員工名稱、員工工資、部門名稱、部門地址
分析:
查詢列:員工名稱、員工工資、部門名稱、部門地址
查詢表:emp和dept,分析得出,不需要外連接(外連接的特性:某一行(或某些行)記錄上會出現一半有值,一半為NULL值)
條件:員工編號為1006
第一步:去除多表,只查一張表,這里去除部門表,只查員工表
SELECT ename, sal FROM emp e WHERE empno=1006 |
第二步:讓第一步與dept做內連接查詢,添加主外鍵條件去除無用笛卡爾積
SELECT e.ename, e.sal, d.dname, d.loc FROM emp e, dept d WHERE e.deptno=d.deptno AND empno=1006 |
第二步中的dept表表示所有行所有列的一張完整的表,這里可以把dept替換成所有行,但只有dname和loc列的表,這需要子查詢。
第三步:查詢dept表中dname和loc兩列,因為deptno會被作為條件,用來去除無用笛卡爾積,所以需要查詢它。
SELECT dname,loc,deptno FROM dept; |
第四步:替換第二步中的dept
SELECT e.ename, e.sal, d.dname, d.loc FROM emp e, (SELECT dname,loc,deptno FROM dept) d WHERE e.deptno=d.deptno AND e.empno=1006 |
l 子查詢作為表
l 子查詢形式為多行多列
Jdbc
JDBC入門
1 什么是JDBC
JDBC(Java DataBase Connectivity)就是Java數據庫連接,說白了就是用Java語言來操作數據庫。原來我們操作數據庫是在控制台使用SQL語句來操作數據庫,JDBC是用Java語言向數據庫發送SQL語句。
2 JDBC原理
早期SUN公司的天才們想編寫一套可以連接天下所有數據庫的API,但是當他們剛剛開始時就發現這是不可完成的任務,因為各個廠商的數據庫服務器差異太大了。后來SUN開始與數據庫廠商們討論,最終得出的結論是,由SUN提供一套訪問數據庫的規范(就是一組接口),並提供連接數據庫的協議標准,然后各個數據庫廠商會遵循SUN的規范提供一套訪問自己公司的數據庫服務器的API出現。SUN提供的規范命名為JDBC,而各個廠商提供的,遵循了JDBC規范的,可以訪問自己數據庫的API被稱之為驅動!
JDBC是接口,而JDBC驅動才是接口的實現,沒有驅動無法完成數據庫連接!每個數據庫廠商都有自己的驅動,用來連接自己公司的數據庫。
當然還有第三方公司專門為某一數據庫提供驅動,這樣的驅動往往不是開源免費的!
3 JDBC核心類(接口)介紹
JDBC中的核心類有:DriverManager、Connection、Statement,和ResultSet!
DriverManger(驅動管理器)的作用有兩個:
l 注冊驅動:這可以讓JDBC知道要使用的是哪個驅動;
l 獲取Connection:如果可以獲取到Connection,那么說明已經與數據庫連接上了。
Connection對象表示連接,與數據庫的通訊都是通過這個對象展開的:
l Connection最為重要的一個方法就是用來獲取Statement對象;
Statement是用來向數據庫發送SQL語句的,這樣數據庫就會執行發送過來的SQL語句:
l void executeUpdate(String sql):執行更新操作(insert、update、delete等);
l ResultSet executeQuery(String sql):執行查詢操作,數據庫在執行查詢后會把查詢結果,查詢結果就是ResultSet;
ResultSet對象表示查詢結果集,只有在執行查詢操作后才會有結果集的產生。結果集是一個二維的表格,有行有列。操作結果集要學習移動ResultSet內部的“行光標”,以及獲取當前行上的每一列上的數據:
l boolean next():使“行光標”(游標)移動到下一行,並返回移動后的行是否存在;
l XXX getXXX(int col):獲取當前行指定列上的值,參數就是列數,列數從1開始,而不是0。
4 Hello JDBC
下面開始編寫第一個JDBC程序
4.1 mysql數據庫的驅動jar包:mysql-connector-java-5.1.13-bin.jar;
4.2 獲取連接
獲取連接需要兩步,一是使用DriverManager來注冊驅動,二是使用DriverManager來獲取Connection對象。
1. 注冊驅動
看清楚了,注冊驅動就只有一句話:Class.forName(“com.mysql.jdbc.Driver”),下面的內容都是對這句代碼的解釋。今后我們的代碼中,與注冊驅動相關的代碼只有這一句。
DriverManager類的registerDriver()方法的參數是java.sql.Driver,但java.sql.Driver是一個接口,實現類由mysql驅動來提供,mysql驅動中的java.sql.Driver接口的實現類為com.mysql.jdbc.Driver!那么注冊驅動的代碼如下:
DriverManager.registerDriver(new com.mysql.jdbc.Driver());
上面代碼雖然可以注冊驅動,但是出現硬編碼(代碼依賴mysql驅動jar包),如果將來想連接Oracle數據庫,那么必須要修改代碼的。並且其實這種注冊驅動的方式是注冊了兩次驅動!
JDBC中規定,驅動類在被加載時,需要自己“主動”把自己注冊到DriverManger中,下面我們來看看com.mysql.jdbc.Driver類的源代碼:
com.mysql.jdbc.Driver.java
public class Driver extends NonRegisteringDriver implements java.sql.Driver { static { try { java.sql.DriverManager.registerDriver(new Driver()); } catch (SQLException E) { throw new RuntimeException("Can't register driver!"); } } …… } |
com.mysql.jdbc.Driver類中的static塊會創建本類對象,並注冊到DriverManager中。這說明只要去加載com.mysql.jdbc.Driver類,那么就會執行這個static塊,從而也就會把com.mysql.jdbc.Driver注冊到DriverManager中,所以可以把注冊驅動類的代碼修改為加載驅動類。
Class.forName(“com.mysql.jdbc.Driver”);
2. 獲取連接
獲取連接的也只有一句代碼:DriverManager.getConnection(url,username,password),其中username和password是登錄數據庫的用戶名和密碼,如果我沒說錯的話,你的mysql數據庫的用戶名和密碼分別是:root、123。
url查對復雜一點,它是用來找到要連接數據庫“網址”,就好比你要瀏覽器中查找百度時,也需要提供一個url。下面是mysql的url:
jdbc:mysql://localhost:3306/mydb1
JDBC規定url的格式由三部分組成,每個部分中間使用逗號分隔。
l 第一部分是jdbc,這是固定的;
l 第二部分是數據庫名稱,那么連接mysql數據庫,第二部分當然是mysql了;
l 第三部分是由數據庫廠商規定的,我們需要了解每個數據庫廠商的要求,mysql的第三部分分別由數據庫服務器的IP地址(localhost)、端口號(3306),以及DATABASE名稱(mydb1)組成。
下面是獲取連接的語句:
Connection con = DriverManager.getConnection(“jdbc:mysql://localhost:3306/mydb1”,”root”,”123”);
還可以在url中提供參數:
jdbc:mysql://localhost:3306/mydb1?useUnicode=true&characterEncoding=UTF8
useUnicode參數指定這個連接數據庫的過程中,使用的字節集是Unicode字節集;
characherEncoding參數指定穿上連接數據庫的過程中,使用的字節集編碼為UTF-8編碼。請注意,mysql中指定UTF-8編碼是給出的是UTF8,而不是UTF-8。要小心了!
4.3 獲取Statement
在得到Connectoin之后,說明已經與數據庫連接上了,下面是通過Connection獲取Statement對象的代碼:
Statement stmt = con.createStatement();
Statement是用來向數據庫發送要執行的SQL語句的!
4.4 發送SQL增、刪、改語句
String sql = “insert into user value(’zhangSan’, ’123’)”;
int m = stmt.executeUpdate(sql);
其中int類型的返回值表示執行這條SQL語句所影響的行數,我們知道,對insert來說,最后只能影響一行,而update和delete可能會影響0~n行。
如果SQL語句執行失敗,那么executeUpdate()會拋出一個SQLException。
4.5 發送SQL查詢語句
String sql = “select * from user”;
ResultSet rs = stmt.executeQuery(sql);
請注冊,執行查詢使用的不是executeUpdate()方法,而是executeQuery()方法。executeQuery()方法返回的是ResultSet,ResultSet封裝了查詢結果,我們稱之為結果集。
4.6 讀取結果集中的數據
ResultSet就是一張二維的表格,它內部有一個“行光標”,光標默認的位置在“第一行上方”,我們可以調用rs對象的next()方法把“行光標”向下移動一行,當第一次調用next()方法時,“行光標”就到了第一行記錄的位置,這時就可以使用ResultSet提供的getXXX(int col)方法來獲取指定列的數據了:
rs.next();//光標移動到第一行
rs.getInt(1);//獲取第一行第一列的數據
當你使用rs.getInt(1)方法時,你必須可以肯定第1列的數據類型就是int類型,如果你不能肯定,那么最好使用rs.getObject(1)。在ResultSet類中提供了一系列的getXXX()方法,比較常用的方法有:
Object getObject(int col)
String getString(int col)
int getInt(int col)
double getDouble(int col)
4.7 關閉
與IO流一樣,使用后的東西都需要關閉!關閉的順序是先得到的后關閉,后得到的先關閉。
rs.close();
stmt.close();
con.close();
4.8 代碼
public static Connection getConnection() throws Exception { Class.forName("com.mysql.jdbc.Driver"); String url = "jdbc:mysql://localhost:3306/mydb1"; return DriverManager.getConnection(url, "root", "123"); } |
@Test public void insert() throws Exception { Connection con = getConnection(); Statement stmt = con.createStatement(); String sql = "insert into user values('zhangSan', '123')"; stmt.executeUpdate(sql); System.out.println("插入成功!"); } |
@Test public void update() throws Exception { Connection con = getConnection(); Statement stmt = con.createStatement(); String sql = "update user set password='456' where username='zhangSan'"; stmt.executeUpdate(sql); System.out.println("修改成功!"); } |
@Test public void delete() throws Exception { Connection con = getConnection(); Statement stmt = con.createStatement(); String sql = "delete from user where username='zhangSan'"; stmt.executeUpdate(sql); System.out.println("刪除成功!"); } |
@Test public void query() throws Exception { Connection con = getConnection(); Statement stmt = con.createStatement(); String sql = "select * from user"; ResultSet rs = stmt.executeQuery(sql); while(rs.next()) { String username = rs.getString(1); String password = rs.getString(2); System.out.println(username + ", " + password); } } |
4.9 規范化代碼
所謂規范化代碼就是無論是否出現異常,都要關閉ResultSet、Statement,以及Connection,如果你還記得IO流的規范化代碼,那么下面的代碼你就明白什么意思了。
@Test public void query() { Connection con = null; Statement stmt = null; ResultSet rs = null; try { con = getConnection(); stmt = con.createStatement(); String sql = "select * from user"; rs = stmt.executeQuery(sql); while(rs.next()) { String username = rs.getString(1); String password = rs.getString(2); System.out.println(username + ", " + password); } } catch(Exception e) { throw new RuntimeException(e); } finally { try { if(rs != null) rs.close(); if(stmt != null) stmt.close(); if(con != null) con.close(); } catch(SQLException e) {} } } |
JDBC對象介紹
1 JDBC中的主要類(接口)
在JDBC中常用的類有:
l DriverManager;
l Connection;
l Statement;
l ResultSet。
2 DriverManager
其實我們今后只需要會用DriverManager的getConnection()方法即可:
1. Class.forName(“com.mysql.jdbc.Driver”);//注冊驅動
2. String url = “jdbc:mysql://localhost:3306/mydb1”;
3. String username = “root”;
4. String password = “123”;
5. Connection con = DriverManager.getConnection(url, username, password);
注意,上面代碼可能出現的兩種異常:
1. ClassNotFoundException:這個異常是在第1句上出現的,出現這個異常有兩個可能:
l 你沒有給出mysql的jar包;
l 你把類名稱打錯了,查看類名是不是com.mysql.jdbc.Driver。
2. SQLException:這個異常出現在第5句,出現這個異常就是三個參數的問題,往往username和password一般不是出錯,所以需要認真查看url是否打錯。
對於DriverManager.registerDriver()方法了解即可,因為我們今后注冊驅動只會Class.forName(),而不會使用這個方法。
3 Connection
Connection最為重要的方法就是獲取Statement:
l Statement stmt = con.createStatement();
后面在學習ResultSet方法時,還要學習一下下面的方法:
l Statement stmt = con.createStatement(int,int);
4 Statement
Statement最為重要的方法是:
l int executeUpdate(String sql):執行更新操作,即執行insert、update、delete語句,其實這個方法也可以執行create table、alter table,以及drop table等語句,但我們很少會使用JDBC來執行這些語句;
l ResultSet executeQuery(String sql):執行查詢操作,執行查詢操作會返回ResultSet,即結果集。
boolean execute()
Statement還有一個boolean execute()方法,這個方法可以用來執行增、刪、改、查所有SQL語句。該方法返回的是boolean類型,表示SQL語句是否執行成功。
如果使用execute()方法執行的是更新語句,那么還要調用int getUpdateCount()來獲取insert、update、delete語句所影響的行數。
如果使用execute()方法執行的是查詢語句,那么還要調用ResultSet getResultSet()來獲取select語句的查詢結果。
5 ResultSet之滾動結果集(了解)
ResultSet表示結果集,它是一個二維的表格!ResultSet內部維護一個行光標(游標),ResultSet提供了一系列的方法來移動游標:
l void beforeFirst():把光標放到第一行的前面,這也是光標默認的位置;
l void afterLast():把光標放到最后一行的后面;
l boolean first():把光標放到第一行的位置上,返回值表示調控光標是否成功;
l boolean last():把光標放到最后一行的位置上;
l boolean isBeforeFirst():當前光標位置是否在第一行前面;
l boolean isAfterLast():當前光標位置是否在最后一行的后面;
l boolean isFirst():當前光標位置是否在第一行上;
l boolean isLast():當前光標位置是否在最后一行上;
l boolean previous():把光標向上挪一行;
l boolean next():把光標向下挪一行;
l boolean relative(int row):相對位移,當row為正數時,表示向下移動row行,為負數時表示向上移動row行;
l boolean absolute(int row):絕對位移,把光標移動到指定的行上;
l int getRow():返回當前光標所有行。
上面方法分為兩類,一類用來判斷游標位置的,另一類是用來移動游標的。如果結果集是不可滾動的,那么只能使用next()方法來移動游標,而beforeFirst()、afterLast()、first()、last()、previous()、relative()方法都不能使用!!!
結果集是否支持滾動,要從Connection類的createStatement()方法說起。也就是說創建的Statement決定了使用Statement創建的ResultSet是否支持滾動。
Statement createStatement(int resultSetType, int resultSetConcurrency)
resultSetType的可選值:
l ResultSet.TYPE_FORWARD_ONLY:不滾動結果集;
l ResultSet.TYPE_SCROLL_INSENSITIVE:滾動結果集,但結果集數據不會再跟隨數據庫而變化;
l ResultSet.TYPE_SCROLL_SENSITIVE:滾動結果集,但結果集數據不會再跟隨數據庫而變化;
可以看出,如果想使用滾動的結果集,我們應該選擇TYPE_SCROLL_INSENSITIVE!其實很少有數據庫驅動會支持TYPE_SCROLL_SENSITIVE的特性!通常我們也不需要查詢到的結果集再受到數據庫變化的影響。
resultSetConcurrency的可選值:
l CONCUR_READ_ONLY:結果集是只讀的,不能通過修改結果集而反向影響數據庫;
l CONCUR_UPDATABLE:結果集是可更新的,對結果集的更新可以反向影響數據庫。
通常可更新結果集這一“高級特性”我們也是不需要的!
獲取滾動結果集的代碼如下:
Connection con = …
Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, CONCUR_READ_ONLY);
String sql = …//查詢語句
ResultSet rs = stmt.executeQuery(sql);//這個結果集是可滾動的
6 ResultSet之獲取列數據
可以通過next()方法使ResultSet的游標向下移動,當游標移動到你需要的行時,就需要來獲取該行的數據了,ResultSet提供了一系列的獲取列數據的方法:
l String getString(int columnIndex):獲取指定列的String類型數據;
l int getInt(int columnIndex):獲取指定列的int類型數據;
l double getDouble(int columnIndex):獲取指定列的double類型數據;
l boolean getBoolean(int columnIndex):獲取指定列的boolean類型數據;
l Object getObject(int columnIndex):獲取指定列的Object類型的數據。
上面方法中,參數columnIndex表示列的索引,列索引從1開始,而不是0,這第一點與數組不同。如果你清楚當前列的數據類型,那么可以使用getInt()之類的方法來獲取,如果你不清楚列的類型,那么你應該使用getObject()方法來獲取。
ResultSet還提供了一套通過列名稱來獲取列數據的方法:
l String getString(String columnName):獲取名稱為columnName的列的String數據;
l int getInt(String columnName):獲取名稱為columnName的列的int數據;
l double getDouble(String columnName):獲取名稱為columnName的列的double數據;
l boolean getBoolean(String columnName):獲取名稱為columnName的列的boolean數據;
l Object getObject(String columnName):獲取名稱為columnName的列的Object數據;
PreparedStatement
1 什么是SQL攻擊
在需要用戶輸入的地方,用戶輸入的是SQL語句的片段,最終用戶輸入的SQL片段與我們DAO中寫的SQL語句合成一個完整的SQL語句!例如用戶在登錄時輸入的用戶名和密碼都是為SQL語句的片段!
2 演示SQL攻擊
首先我們需要創建一張用戶表,用來存儲用戶的信息。
CREATE TABLE user( uidCHAR(32) PRIMARY KEY, usernameVARCHAR(30) UNIQUE KEY NOT NULL, PASSWORD VARCHAR(30) );
INSERT INTO user VALUES('U_1001', 'zs', 'zs'); SELECT * FROM user; |
現在用戶表中只有一行記錄,就是zs。
下面我們寫一個login()方法!
public void login(String username, String password) { Connection con = null; Statement stmt = null; ResultSet rs = null; try { con = JdbcUtils.getConnection(); stmt = con.createStatement(); String sql = "SELECT * FROM user WHERE " + "username='" + username + "' and password='" + password + "'"; rs = stmt.executeQuery(sql); if(rs.next()) { System.out.println("歡迎" + rs.getString("username")); } else { System.out.println("用戶名或密碼錯誤!"); } } catch (Exception e) { throw new RuntimeException(e); } finally { JdbcUtils.close(con, stmt, rs); } } |
下面是調用這個方法的代碼:
login("a' or 'a'='a", "a' or 'a'='a"); |
這行當前會使我們登錄成功!因為是輸入的用戶名和密碼是SQL語句片段,最終與我們的login()方法中的SQL語句組合在一起!我們來看看組合在一起的SQL語句:
SELECT * FROM tab_user WHERE username='a' or 'a'='a' and password='a' or 'a'='a' |
3 防止SQL攻擊
l 過濾用戶輸入的數據中是否包含非法字符;
l 分步交驗!先使用用戶名來查詢用戶,如果查找到了,再比較密碼;
l 使用PreparedStatement。
4 PreparedStatement是什么?
PreparedStatement叫預編譯聲明!
PreparedStatement是Statement的子接口,你可以使用PreparedStatement來替換Statement。
PreparedStatement的好處:
l 防止SQL攻擊;
l 提高代碼的可讀性,以可維護性;
l 提高效率。
5 PreparedStatement的使用
String sql = “select * from tab_student where s_number=?”; PreparedStatement pstmt = con.prepareStatement(sql); pstmt.setString(1, “S_1001”); ResultSet rs = pstmt.executeQuery(); rs.close(); pstmt.clearParameters(); pstmt.setString(1, “S_1002”); rs = pstmt.executeQuery(); |
在使用Connection創建PreparedStatement對象時需要給出一個SQL模板,所謂SQL模板就是有“?”的SQL語句,其中“?”就是參數。
在得到PreparedStatement對象后,調用它的setXXX()方法為“?”賦值,這樣就可以得到把模板變成一條完整的SQL語句,然后再調用PreparedStatement對象的executeQuery()方法獲取ResultSet對象。
注意PreparedStatement對象獨有的executeQuery()方法是沒有參數的,而Statement的executeQuery()是需要參數(SQL語句)的。因為在創建PreparedStatement對象時已經讓它與一條SQL模板綁定在一起了,所以在調用它的executeQuery()和executeUpdate()方法時就不再需要參數了。
PreparedStatement最大的好處就是在於重復使用同一模板,給予其不同的參數來重復的使用它。這才是真正提高效率的原因。
所以,建議大家在今后的開發中,無論什么情況,都去需要PreparedStatement,而不是使用Statement。
JdbcUtils工具類
1 JdbcUtils的作用
你也看到了,連接數據庫的四大參數是:驅動類、url、用戶名,以及密碼。這些參數都與特定數據庫關聯,如果將來想更改數據庫,那么就要去修改這四大參數,那么為了不去修改代碼,我們寫一個JdbcUtils類,讓它從配置文件中讀取配置參數,然后創建連接對象。
2 JdbcUtils代碼
JdbcUtils.java
public class JdbcUtils { private static final String dbconfig = "dbconfig.properties"; private static Properties prop = new Properties(); static { try { InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream(dbconfig); prop.load(in); Class.forName(prop.getProperty("driverClassName")); } catch(IOException e) { throw new RuntimeException(e); } } public static Connection getConnection() { try { return DriverManager.getConnection(prop.getProperty("url"), prop.getProperty("username"), prop.getProperty("password")); } catch (Exception e) { throw new RuntimeException(e); } } } |
dbconfig.properties
driverClassName=com.mysql.jdbc.Driver url=jdbc:mysql://localhost:3306/mydb1?useUnicode=true&characterEncoding=UTF8 username=root password=123 |
UserDao
1 DAO模式
DAO(Data Access Object)模式就是寫一個類,把訪問數據庫的代碼封裝起來。DAO在數據庫與業務邏輯(Service)之間。
l 實體域,即操作的對象,例如我們操作的表是user表,那么就需要先寫一個User類;
l DAO模式需要先提供一個DAO接口;
l 然后再提供一個DAO接口的實現類;
l 再編寫一個DAO工廠,Service通過工廠來獲取DAO實現。
2 代碼
User.java
public class User { private String uid; private String username; private String password; … } |
UserDao.java
public interface UserDao { public void add(User user); public void mod(User user); public void del(String uid); public User load(String uid); public List<User> findAll(); } |
UserDaoImpl.java
public class UserDaoImpl implements UserDao { public void add(User user) { Connection con = null; PreparedStatement pstmt = null; try { con = JdbcUtils.getConnection(); String sql = "insert into user value(?,?,?)"; pstmt = con.prepareStatement(sql); pstmt.setString(1, user.getUid()); pstmt.setString(2, user.getUsername()); pstmt.setString(3, user.getPassword()); pstmt.executeUpdate(); } catch(Exception e) { throw new RuntimeException(e); } finally { try { if(pstmt != null) pstmt.close(); if(con != null) con.close(); } catch(SQLException e) {} } } public void mod(User user) { Connection con = null; PreparedStatement pstmt = null; try { con = JdbcUtils.getConnection(); String sql = "update user set username=?, password=? where uid=?"; pstmt = con.prepareStatement(sql); pstmt.setString(1, user.getUsername()); pstmt.setString(2, user.getPassword()); pstmt.setString(3, user.getUid()); pstmt.executeUpdate(); } catch(Exception e) { throw new RuntimeException(e); } finally { try { if(pstmt != null) pstmt.close(); if(con != null) con.close(); } catch(SQLException e) {} } } public void del(String uid) { Connection con = null; PreparedStatement pstmt = null; try { con = JdbcUtils.getConnection(); String sql = "delete from user where uid=?"; pstmt = con.prepareStatement(sql); pstmt.setString(1, uid); pstmt.executeUpdate(); } catch(Exception e) { throw new RuntimeException(e); } finally { try { if(pstmt != null) pstmt.close(); if(con != null) con.close(); } catch(SQLException e) {} } } public User load(String uid) { Connection con = null; PreparedStatement pstmt = null; ResultSet rs = null; try { con = JdbcUtils.getConnection(); String sql = "select * from user where uid=?"; pstmt = con.prepareStatement(sql); pstmt.setString(1, uid); rs = pstmt.executeQuery(); if(rs.next()) { return new User(rs.getString(1), rs.getString(2), rs.getString(3)); } return null; } catch(Exception e) { throw new RuntimeException(e); } finally { try { if(pstmt != null) pstmt.close(); if(con != null) con.close(); } catch(SQLException e) {} } } public List<User> findAll() { Connection con = null; PreparedStatement pstmt = null; ResultSet rs = null; try { con = JdbcUtils.getConnection(); String sql = "select * from user"; pstmt = con.prepareStatement(sql); rs = pstmt.executeQuery(); List<User> userList = new ArrayList<User>(); while(rs.next()) { userList.add(new User(rs.getString(1), rs.getString(2), rs.getString(3))); } return userList; } catch(Exception e) { throw new RuntimeException(e); } finally { try { if(pstmt != null) pstmt.close(); if(con != null) con.close(); } catch(SQLException e) {} } } } |
UserDaoFactory.java
public class UserDaoFactory { private static UserDao userDao; static { try { InputStream in = Thread.currentThread().getContextClassLoader() .getResourceAsStream("dao.properties"); Properties prop = new Properties(); prop.load(in); String className = prop.getProperty("cn.itcast.jdbc.UserDao"); Class clazz = Class.forName(className); userDao = (UserDao) clazz.newInstance(); } catch (Exception e) { throw new RuntimeException(e); } } public static UserDao getUserDao() { return userDao; } } |
dao.properties
cn.itcast.jdbc.UserDao=cn.itcast.jdbc.UserDaoImpl |
時間類型
1 Java中的時間類型
java.sql包下給出三個與數據庫相關的日期時間類型,分別是:
l Date:表示日期,只有年月日,沒有時分秒。會丟失時間;
l Time:表示時間,只有時分秒,沒有年月日。會丟失日期;
l Timestamp:表示時間戳,有年月日時分秒,以及毫秒。
這三個類都是java.util.Date的子類。
2 時間類型相互轉換
把數據庫的三種時間類型賦給java.util.Date,基本不用轉換,因為這是把子類對象給父類的引用,不需要轉換。
java.sql.Date date = …
java.util.Date d = date;
java.sql.Time time = …
java.util.Date d = time;
java.sql.Timestamp timestamp = …
java.util.Date d = timestamp;
當需要把java.util.Date轉換成數據庫的三種時間類型時,這就不能直接賦值了,這需要使用數據庫三種時間類型的構造器。java.sql包下的Date、Time、TimeStamp三個類的構造器都需要一個long類型的參數,表示毫秒值。創建這三個類型的對象,只需要有毫秒值即可。我們知道java.util.Date有getTime()方法可以獲取毫秒值,那么這個轉換也就不是什么問題了。
java.utl.Date d = new java.util.Date();
java.sql.Date date = new java.sql.Date(d.getTime());//會丟失時分秒
Time time = new Time(d.getTime());//會丟失年月日
Timestamp timestamp = new Timestamp(d.getTime());
3 代碼
我們來創建一個dt表:
CREATE TABLE dt( d DATE, t TIME, ts TIMESTAMP ) |
下面是向dt表中插入數據的代碼:
@Test public void fun1() throws SQLException { Connection con = JdbcUtils.getConnection(); String sql = "insert into dt value(?,?,?)"; PreparedStatement pstmt = con.prepareStatement(sql); java.util.Date d = new java.util.Date(); pstmt.setDate(1, new java.sql.Date(d.getTime())); pstmt.setTime(2, new Time(d.getTime())); pstmt.setTimestamp(3, new Timestamp(d.getTime())); pstmt.executeUpdate(); } |
下面是從dt表中查詢數據的代碼:
@Test public void fun2() throws SQLException { Connection con = JdbcUtils.getConnection(); String sql = "select * from dt"; PreparedStatement pstmt = con.prepareStatement(sql); ResultSet rs = pstmt.executeQuery(); rs.next(); java.util.Date d1 = rs.getDate(1); java.util.Date d2 = rs.getTime(2); java.util.Date d3 = rs.getTimestamp(3); System.out.println(d1); System.out.println(d2); System.out.println(d3); } |
大數據
1 什么是大數據
所謂大數據,就是大的字節數據,或大的字符數據。標准SQL中提供了如下類型來保存大數據類型:
類型 |
長度 |
tinyblob |
28--1B(256B) |
blob |
216-1B(64K) |
mediumblob |
224-1B(16M) |
longblob |
232-1B(4G) |
tinyclob |
28--1B(256B) |
clob |
216-1B(64K) |
mediumclob |
224-1B(16M) |
longclob |
232-1B(4G) |
但是,在mysql中沒有提供tinyclob、clob、mediumclob、longclob四種類型,而是使用如下四種類型來處理文本大數據:
類型 |
長度 |
tinytext |
28--1B(256B) |
text |
216-1B(64K) |
mediumtext |
224-1B(16M) |
longtext |
232-1B(4G) |
首先我們需要創建一張表,表中要有一個mediumblob(16M)類型的字段。
CREATE TABLE tab_bin( id INT PRIMARY KEY AUTO_INCREMENT, filenameVARCHAR(100), dataMEDIUMBLOB ); |
向數據庫插入二進制數據需要使用PreparedStatement為原setBinaryStream(int, InputSteam)方法來完成。
con = JdbcUtils.getConnection(); String sql = "insert into tab_bin(filename,data) values(?, ?)"; pstmt = con.prepareStatement(sql); pstmt.setString(1, "a.jpg"); InputStream in = new FileInputStream("f:\\a.jpg"); pstmt.setBinaryStream(2, in); pstmt.executeUpdate(); |
讀取二進制數據,需要在查詢后使用ResultSet類的getBinaryStream()方法來獲取輸入流對象。也就是說,PreparedStatement有setXXX(),那么ResultSet就有getXXX()。
con = JdbcUtils.getConnection(); String sql = "select filename,data from tab_bin where id=?"; pstmt = con.prepareStatement(sql); pstmt.setInt(1, 1); rs = pstmt.executeQuery(); rs.next(); String filename = rs.getString("filename"); OutputStream out = new FileOutputStream("F:\\" + filename); InputStream in = rs.getBinaryStream("data"); IOUtils.copy(in, out); out.close(); |
還有一種方法,就是把要存儲的數據包裝成Blob類型,然后調用PreparedStatement的setBlob()方法來設置數據
con = JdbcUtils.getConnection(); String sql = "insert into tab_bin(filename,data) values(?, ?)"; pstmt = con.prepareStatement(sql); pstmt.setString(1, "a.jpg"); File file = new File("f:\\a.jpg"); byte[] datas = FileUtils.getBytes(file);//獲取文件中的數據 Blob blob = new SerialBlob(datas);//創建Blob對象 pstmt.setBlob(2, blob);//設置Blob類型的參數 pstmt.executeUpdate(); |
con = JdbcUtils.getConnection(); String sql = "select filename,data from tab_bin where id=?"; pstmt = con.prepareStatement(sql); pstmt.setInt(1, 1); rs = pstmt.executeQuery(); rs.next(); String filename = rs.getString("filename"); File file = new File("F:\\" + filename) ; Blob blob = rs.getBlob("data"); byte[] datas = blob.getBytes(0, (int)file.length()); FileUtils.writeByteArrayToFile(file, datas); |
批處理
1 Statement批處理
批處理就是一批一批的處理,而不是一個一個的處理!
當你有10條SQL語句要執行時,一次向服務器發送一條SQL語句,這么做效率上很差!處理的方案是使用批處理,即一次向服務器發送多條SQL語句,然后由服務器一次性處理。
批處理只針對更新(增、刪、改)語句,批處理沒有查詢什么事兒!
可以多次調用Statement類的addBatch(String sql)方法,把需要執行的所有SQL語句添加到一個“批”中,然后調用Statement類的executeBatch()方法來執行當前“批”中的語句。
l void addBatch(String sql):添加一條語句到“批”中;
l int[] executeBatch():執行“批”中所有語句。返回值表示每條語句所影響的行數據;
l void clearBatch():清空“批”中的所有語句。
for(int i = 0; i < 10; i++) { String number = "S_10" + i; String name = "stu" + i; int age = 20 + i; String gender = i % 2 == 0 ? "male" : "female"; String sql = "insert into stu values('" + number + "', '" + name + "', " + age + ", '" + gender + "')"; stmt.addBatch(sql); } stmt.executeBatch(); |
當執行了“批”之后,“批”中的SQL語句就會被清空!也就是說,連續兩次調用executeBatch()相當於調用一次!因為第二次調用時,“批”中已經沒有SQL語句了。
還可以在執行“批”之前,調用Statement的clearBatch()方法來清空“批”!
2 PreparedStatement批處理
PreparedStatement的批處理有所不同,因為每個PreparedStatement對象都綁定一條SQL模板。所以向PreparedStatement中添加的不是SQL語句,而是給“?”賦值。
con = JdbcUtils.getConnection(); String sql = "insert into stu values(?,?,?,?)"; pstmt = con.prepareStatement(sql); for(int i = 0; i < 10; i++) { pstmt.setString(1, "S_10" + i); pstmt.setString(2, "stu" + i); pstmt.setInt(3, 20 + i); pstmt.setString(4, i % 2 == 0 ? "male" : "female"); pstmt.addBatch(); } pstmt.executeBatch(); |
jdbc事務
事務概述
為了方便演示事務,我們需要創建一個account表:
CREATE TABLE account( id INT PRIMARY KEY AUTO_INCREMENT, NAME VARCHAR(30), balance NUMERIC(10.2) );
INSERT INTO account(NAME,balance) VALUES('zs', 100000); INSERT INTO account(NAME,balance) VALUES('ls', 100000); INSERT INTO account(NAME,balance) VALUES('ww', 100000);
SELECT * FROM account; |
1 什么是事務
銀行轉賬!張三轉10000塊到李四的賬戶,這其實需要兩條SQL語句:
l 給張三的賬戶減去10000元;
l 給李四的賬戶加上10000元。
如果在第一條SQL語句執行成功后,在執行第二條SQL語句之前,程序被中斷了(可能是拋出了某個異常,也可能是其他什么原因),那么李四的賬戶沒有加上10000元,而張三卻減去了10000元。這肯定是不行的!
你現在可能已經知道什么是事務了吧!事務中的多個操作,要么完全成功,要么完全失敗!不可能存在成功一半的情況!也就是說給張三的賬戶減去10000元如果成功了,那么給李四的賬戶加上10000元的操作也必須是成功的;否則給張三減去10000元,以及給李四加上10000元都是失敗的!
2 事務的四大特性(ACID)
面試!
事務的四大特性是:
l 原子性(Atomicity):事務中所有操作是不可再分割的原子單位。事務中所有操作要么全部執行成功,要么全部執行失敗。
l 一致性(Consistency):事務執行后,數據庫狀態與其它業務規則保持一致。如轉賬業務,無論事務執行成功與否,參與轉賬的兩個賬號余額之和應該是不變的。
l 隔離性(Isolation):隔離性是指在並發操作中,不同事務之間應該隔離開來,使每個並發中的事務不會相互干擾。
l 持久性(Durability):一旦事務提交成功,事務中所有的數據操作都必須被持久化到數據庫中,即使提交事務后,數據庫馬上崩潰,在數據庫重啟時,也必須能保證通過某種機制恢復數據。
3 MySQL中的事務
在默認情況下,MySQL每執行一條SQL語句,都是一個單獨的事務。如果需要在一個事務中包含多條SQL語句,那么需要開啟事務和結束事務。
l 開啟事務:start transaction;
l 結束事務:commit或rollback。
在執行SQL語句之前,先執行strat transaction,這就開啟了一個事務(事務的起點),然后可以去執行多條SQL語句,最后要結束事務,commit表示提交,即事務中的多條SQL語句所做出的影響會持久化到數據庫中。或者rollback,表示回滾,即回滾到事務的起點,之前做的所有操作都被撤消了!
下面演示zs給li轉賬10000元的示例:
START TRANSACTION; UPDATE account SET balance=balance-10000 WHERE id=1; UPDATE account SET balance=balance+10000 WHERE id=2; ROLLBACK; |
START TRANSACTION; UPDATE account SET balance=balance-10000 WHERE id=1; UPDATE account SET balance=balance+10000 WHERE id=2; COMMIT; |
START TRANSACTION; UPDATE account SET balance=balance-10000 WHERE id=1; UPDATE account SET balance=balance+10000 WHERE id=2; quit; |
JDBC事務
1 JDBC中的事務
Connection的三個方法與事務相關:
l setAutoCommit(boolean):設置是否為自動提交事務,如果true(默認值就是true)表示自動提交,也就是每條執行的SQL語句都是一個單獨的事務,如果設置false,那么就相當於開啟了事務了;
l commit():提交結束事務;
l rollback():回滾結束事務。
public void transfer(boolean b) { Connection con = null; PreparedStatement pstmt = null; try { con = JdbcUtils.getConnection(); //手動提交 con.setAutoCommit(false); String sql = "update account set balance=balance+? where id=?"; pstmt = con.prepareStatement(sql); //操作 pstmt.setDouble(1, -10000); pstmt.setInt(2, 1); pstmt.executeUpdate(); // 在兩個操作中拋出異常 if(b) { throw new Exception(); } pstmt.setDouble(1, 10000); pstmt.setInt(2, 2); pstmt.executeUpdate(); //提交事務 con.commit(); } catch(Exception e) { //回滾事務 if(con != null) { try { con.rollback(); } catch(SQLException ex) {} } throw new RuntimeException(e); } finally { //關閉 JdbcUtils.close(con, pstmt); } } |
2 保存點(了解)
保存點是JDBC3.0的東西!當要求數據庫服務器支持保存點方式的回滾。
校驗數據庫服務器是否支持保存點!
boolean b = con.getMetaData().supportsSavepoints(); |
保存點的作用是允許事務回滾到指定的保存點位置。在事務中設置好保存點,然后回滾時可以選擇回滾到指定的保存點,而不是回滾整個事務!注意,回滾到指定保存點並沒有結束事務!!!只有回滾了整個事務才算是結束事務了!
Connection類的設置保存點,以及回滾到指定保存點方法:
l 設置保存點:Savepoint setSavepoint();
l 回滾到指定保存點:void rollback(Savepoint)。
/* * 李四對張三說,如果你給我轉1W,我就給你轉100W。 * ========================================== * * 張三給李四轉1W(張三減去1W,李四加上1W) * 設置保存點! * 李四給張三轉100W(李四減去100W,張三加上100W) * 查看李四余額為負數,那么回滾到保存點。 * 提交事務 */ @Test public void fun() { Connection con = null; PreparedStatement pstmt = null; try { con = JdbcUtils.getConnection(); //手動提交 con.setAutoCommit(false); String sql = "update account set balance=balance+? where name=?"; pstmt = con.prepareStatement(sql); //操作1(張三減去1W) pstmt.setDouble(1, -10000); pstmt.setString(2, "zs"); pstmt.executeUpdate(); //操作2(李四加上1W) pstmt.setDouble(1, 10000); pstmt.setString(2, "ls"); pstmt.executeUpdate(); // 設置保存點 Savepoint sp = con.setSavepoint(); //操作3(李四減去100W) pstmt.setDouble(1, -1000000); pstmt.setString(2, "ls"); pstmt.executeUpdate(); //操作4(張三加上100W) pstmt.setDouble(1, 1000000); pstmt.setString(2, "zs"); pstmt.executeUpdate(); //操作5(查看李四余額) sql = "select balance from account where name=?"; pstmt = con.prepareStatement(sql); pstmt.setString(1, "ls"); ResultSet rs = pstmt.executeQuery(); rs.next(); double balance = rs.getDouble(1); //如果李四余額為負數,那么回滾到指定保存點 if(balance < 0) { con.rollback(sp); System.out.println("張三,你上當了!"); } //提交事務 con.commit(); } catch(Exception e) { //回滾事務 if(con != null) { try { con.rollback(); } catch(SQLException ex) {} } throw new RuntimeException(e); } finally { //關閉 JdbcUtils.close(con, pstmt); } } |
事務隔離級別
事務的並發讀問題
l 臟讀:讀取到另一個事務未提交數據;
l 不可重復讀:兩次讀取不一致;
l 幻讀(虛讀):讀到另一事務已提交數據。
2 並發事務問題
因為並發事務導致的問題大致有5類,其中兩類是更新問題,三類是讀問題。
l 臟讀(dirty read):讀到未提交更新數據,即讀取到了臟數據;
l 不可重復讀(unrepeatable read):對同一記錄的兩次讀取不一致,因為另一事務對該記錄做了修改;
l 幻讀(phantom read):對同一張表的兩次查詢不一致,因為另一事務插入了一條記錄;
臟讀
事務1:張三給李四轉賬100元
事務2:李四查看自己的賬戶
l t1:事務1:開始事務
l t2:事務1:張三給李四轉賬100元
l t3:事務2:開始事務
l t4:事務2:李四查看自己的賬戶,看到賬戶多出100元(臟讀)
l t5:事務2:提交事務
l t6:事務1:回滾事務,回到轉賬之前的狀態
不可重復讀
事務1:酒店查看兩次1048號房間狀態
事務2:預訂1048號房間
l t1:事務1:開始事務
l t2:事務1:查看1048號房間狀態為空閑
l t3:事務2:開始事務
l t4:事務2:預定1048號房間
l t5:事務2:提交事務
l t6:事務1:再次查看1048號房間狀態為使用
l t7:事務1:提交事務
對同一記錄的兩次查詢結果不一致!
幻讀
事務1:對酒店房間預訂記錄兩次統計
事務2:添加一條預訂房間記錄
l t1:事務1:開始事務
l t2:事務1:統計預訂記錄100條
l t3:事務2:開始事務
l t4:事務2:添加一條預訂房間記錄
l t5:事務2:提交事務
l t6:事務1:再次統計預訂記錄為101記錄
l t7:事務1:提交
對同一表的兩次查詢不一致!
不可重復讀和幻讀的區別:
l 不可重復讀是讀取到了另一事務的更新;
l 幻讀是讀取到了另一事務的插入(MySQL中無法測試到幻讀);
3 四大隔離級別
4個等級的事務隔離級別,在相同數據環境下,使用相同的輸入,執行相同的工作,根據不同的隔離級別,可以導致不同的結果。不同事務隔離級別能夠解決的數據並發問題的能力是不同的。
1 SERIALIZABLE(串行化)
l 不會出現任何並發問題,因為它是對同一數據的訪問是串行的,非並發訪問的;
l 性能最差;
2 REPEATABLE READ(可重復讀)
l 防止臟讀和不可重復讀;(不能處理幻讀)
l 性能比SERIALIZABLE好
3 READ COMMITTED(讀已提交數據)
l 防止臟讀;(不能處理不可重復讀、幻讀)
l 性能比REPEATABLE READ好
4 READ UNCOMMITTED(讀未提交數據)
l 可能出現任何事務並發問題
l 性能最好
MySQL的默認隔離級別為REPEATABLE READ,這是一個很不錯的選擇吧!
4 MySQL隔離級別
MySQL的默認隔離級別為Repeatable read,可以通過下面語句查看:
select @@tx_isolation |
也可以通過下面語句來設置當前連接的隔離級別:
set transaction isolationlevel [4先1] |
5 JDBC設置隔離級別
con. setTransactionIsolation(int level)
參數可選值如下:
l Connection.TRANSACTION_READ_UNCOMMITTED;
l Connection.TRANSACTION_READ_COMMITTED;
l Connection.TRANSACTION_REPEATABLE_READ;
l Connection.TRANSACTION_SERIALIZABLE。
事務總結:
l 事務的特性:ACID;
l 事務開始邊界與結束邊界:開始邊界(con.setAutoCommit(false)),結束邊界(con.commit()或con.rollback());
l 事務的隔離級別: READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ、SERIALIZABLE。多個事務並發執行時才需要考慮並發事務。
數據庫連接池、dbutil
數據庫連接池
1 數據庫連接池的概念
用池來管理Connection,這可以重復使用Connection。有了池,所以我們就不用自己來創建Connection,而是通過池來獲取Connection對象。當使用完Connection后,調用Connection的close()方法也不會真的關閉Connection,而是把Connection“歸還”給池。池就可以再利用這個Connection對象了。
2 JDBC數據庫連接池接口(DataSource)
Java為數據庫連接池提供了公共的接口:javax.sql.DataSource,各個廠商可以讓自己的連接池實現這個接口。這樣應用程序可以方便的切換不同廠商的連接池!
3 自定義連接池(ItcastPool)
分析:ItcastPool需要有一個List,用來保存連接對象。在ItcastPool的構造器中創建5個連接對象放到List中!當用人調用了ItcastPool的getConnection()時,那么就從List拿出一個返回。當List中沒有連接可用時,拋出異常。
我們需要對Connection的close()方法進行增強,所以我們需要自定義ItcastConnection類,對Connection進行裝飾!即對close()方法進行增強。因為需要在調用close()方法時把連接“歸還”給池,所以ItcastConnection類需要擁有池對象的引用,並且池類還要提供“歸還”的方法。
ItcastPool.java
public class ItcastPool implements DataSource { private static Properties props = new Properties(); private List<Connection> list = new ArrayList<Connection>(); static { InputStream in = ItcastPool.class.getClassLoader() .getResourceAsStream("dbconfig.properties"); try { props.load(in); Class.forName(props.getProperty("driverClassName")); } catch (Exception e) { throw new RuntimeException(e); } } public ItcastPool() throws SQLException { for (int i = 0; i < 5; i++) { Connection con = DriverManager.getConnection( props.getProperty("url"), props.getProperty("username"), props.getProperty("password")); ItcastConnection conWapper = new ItcastConnection(con, this); list.add(conWapper); } } public void add(Connection con) { list.add(con); } public Connection getConnection() throws SQLException { if(list.size() > 0) { return list.remove(0); } throw new SQLException("沒連接了"); } ...... } |
ItcastConnection.java
public class ItcastConnection extends ConnectionWrapper { private ItcastPool pool; public ItcastConnection(Connection con, ItcastPool pool) { super(con); this.pool = pool; } @Override public void close() throws SQLException { pool.add(this); } } |
DBCP
1 什么是DBCP?
DBCP是Apache提供的一款開源免費的數據庫連接池!
Hibernate3.0之后不再對DBCP提供支持!因為Hibernate聲明DBCP有致命的缺欠!DBCP因為Hibernate的這一毀謗很是生氣,並且說自己沒有缺欠。
2 DBCP的使用
public void fun1() throws SQLException { BasicDataSource ds = new BasicDataSource(); ds.setUsername("root"); ds.setPassword("123"); ds.setUrl("jdbc:mysql://localhost:3306/mydb1"); ds.setDriverClassName("com.mysql.jdbc.Driver"); ds.setMaxActive(20); ds.setMaxIdle(10); ds.setInitialSize(10); ds.setMinIdle(2); ds.setMaxWait(1000); Connection con = ds.getConnection(); System.out.println(con.getClass().getName()); con.close(); } |
3 DBCP的配置信息
下面是對DBCP的配置介紹:
#基本配置 driverClassName=com.mysql.jdbc.Driver url=jdbc:mysql://localhost:3306/mydb1 username=root password=123 #初始化池大小,即一開始池中就會有10個連接對象 默認值為0 initialSize=0 #最大連接數,如果設置maxActive=50時,池中最多可以有50個連接,當然這50個連接中包含被使用的和沒被使用的(空閑) #你是一個包工頭,你一共有50個工人,但這50個工人有的當前正在工作,有的正在空閑 #默認值為8,如果設置為非正數,表示沒有限制!即無限大 maxActive=8 #最大空閑連接 #當設置maxIdle=30時,你是包工頭,你允許最多有20個工人空閑,如果現在有30個空閑工人,那么要開除10個 #默認值為8,如果設置為負數,表示沒有限制!即無限大 maxIdle=8 #最小空閑連接 #如果設置minIdel=5時,如果你的工人只有3個空閑,那么你需要再去招2個回來,保證有5個空閑工人 #默認值為0 minIdle=0 #最大等待時間 #當設置maxWait=5000時,現在你的工作都出去工作了,又來了一個工作,需要一個工人。 #這時就要等待有工人回來,如果等待5000毫秒還沒回來,那就拋出異常 #沒有工人的原因:最多工人數為50,已經有50個工人了,不能再招了,但50人都出去工作了。 #默認值為-1,表示無限期等待,不會拋出異常。 maxWait=-1 #連接屬性 #就是原來放在url后面的參數,可以使用connectionProperties來指定 #如果已經在url后面指定了,那么就不用在這里指定了。 #useServerPrepStmts=true,MySQL開啟預編譯功能 #cachePrepStmts=true,MySQL開啟緩存PreparedStatement功能, #prepStmtCacheSize=50,緩存PreparedStatement的上限 #prepStmtCacheSqlLimit=300,當SQL模板長度大於300時,就不再緩存它 connectionProperties=useUnicode=true;characterEncoding=UTF8;useServerPrepStmts=true;cachePrepStmts=true;prepStmtCacheSize=50;prepStmtCacheSqlLimit=300 #連接的默認提交方式 #默認值為true defaultAutoCommit=true #連接是否為只讀連接 #Connection有一對方法:setReadOnly(boolean)和isReadOnly() #如果是只讀連接,那么你只能用這個連接來做查詢 #指定連接為只讀是為了優化!這個優化與並發事務相關! #如果兩個並發事務,對同一行記錄做增、刪、改操作,是不是一定要隔離它們啊? #如果兩個並發事務,對同一行記錄只做查詢操作,那么是不是就不用隔離它們了? #如果沒有指定這個屬性值,那么是否為只讀連接,這就由驅動自己來決定了。即Connection的實現類自己來決定! defaultReadOnly=false #指定事務的事務隔離級別 #可選值:NONE,READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE #如果沒有指定,那么由驅動中的Connection實現類自己來決定 defaultTransactionIsolation=REPEATABLE_READ |
C3P0
1 C3P0簡介
C3P0也是開源免費的連接池!C3P0被很多人看好!
2 C3P0的使用
C3P0中池類是:ComboPooledDataSource。
public void fun1() throws PropertyVetoException, SQLException { ComboPooledDataSource ds = new ComboPooledDataSource(); ds.setJdbcUrl("jdbc:mysql://localhost:3306/mydb1"); ds.setUser("root"); ds.setPassword("123"); ds.setDriverClass("com.mysql.jdbc.Driver"); ds.setAcquireIncrement(5); ds.setInitialPoolSize(20); ds.setMinPoolSize(2); ds.setMaxPoolSize(50); Connection con = ds.getConnection(); System.out.println(con); con.close(); } |
c3p0也可以指定配置文件,而且配置文件可以是properties,也可騍xml的。當然xml的高級一些了。但是c3p0的配置文件名必須為c3p0-config.xml,並且必須放在類路徑下。
<?xml version="1.0" encoding="UTF-8"?> <c3p0-config> <default-config> <property name="jdbcUrl">jdbc:mysql://localhost:3306/mydb1</property> <property name="driverClass">com.mysql.jdbc.Driver</property> <property name="user">root</property> <property name="password">123</property> <property name="acquireIncrement">3</property> <property name="initialPoolSize">10</property> <property name="minPoolSize">2</property> <property name="maxPoolSize">10</property> </default-config> <named-config name="oracle-config"> <property name="jdbcUrl">jdbc:mysql://localhost:3306/mydb1</property> <property name="driverClass">com.mysql.jdbc.Driver</property> <property name="user">root</property> <property name="password">123</property> <property name="acquireIncrement">3</property> <property name="initialPoolSize">10</property> <property name="minPoolSize">2</property> <property name="maxPoolSize">10</property> </named-config> </c3p0-config> |
c3p0的配置文件中可以配置多個連接信息,可以給每個配置起個名字,這樣可以方便的通過配置名稱來切換配置信息。上面文件中默認配置為mysql的配置,名為oracle-config的配置也是mysql的配置,呵呵。
public void fun2() throws PropertyVetoException, SQLException { ComboPooledDataSource ds = new ComboPooledDataSource(); Connection con = ds.getConnection(); System.out.println(con); con.close(); } |
public void fun2() throws PropertyVetoException, SQLException { ComboPooledDataSource ds = new ComboPooledDataSource("orcale-config"); Connection con = ds.getConnection(); System.out.println(con); con.close(); } |
Tomcat配置連接池
1 Tomcat配置JNDI資源
JNDI(Java Naming and Directory Interface),Java命名和目錄接口。JNDI的作用就是:在服務器上配置資源,然后通過統一的方式來獲取配置的資源。
我們這里要配置的資源當然是連接池了,這樣項目中就可以通過統一的方式來獲取連接池對象了。
下圖是Tomcat文檔提供的:
配置JNDI資源需要到<Context>元素中配置<Resource>子元素:
l name:指定資源的名稱,這個名稱可以隨便給,在獲取資源時需要這個名稱;
l factory:用來創建資源的工廠,這個值基本上是固定的,不用修改;
l type:資源的類型,我們要給出的類型當然是我們連接池的類型了;
l bar:表示資源的屬性,如果資源存在名為bar的屬性,那么就配置bar的值。對於DBCP連接池而言,你需要配置的不是bar,因為它沒有bar這個屬性,而是應該去配置url、username等屬性。
<Context> <Resource name="mydbcp" type="org.apache.tomcat.dbcp.dbcp.BasicDataSource" factory="org.apache.naming.factory.BeanFactory" username="root" password="123" driverClassName="com.mysql.jdbc.Driver" url="jdbc:mysql://127.0.0.1/mydb1" maxIdle="3" maxWait="5000" maxActive="5" initialSize="3"/> </Context> |
<Context> <Resource name="myc3p0" type="com.mchange.v2.c3p0.ComboPooledDataSource" factory="org.apache.naming.factory.BeanFactory" user="root" password="123" classDriver="com.mysql.jdbc.Driver" jdbcUrl="jdbc:mysql://127.0.0.1/mydb1" maxPoolSize="20" minPoolSize ="5" initialPoolSize="10" acquireIncrement="2"/> </Context> |
2 獲取資源
配置資源的目的當然是為了獲取資源了。只要你啟動了Tomcat,那么就可以在項目中任何類中通過JNDI獲取資源的方式來獲取資源了。
下圖是Tomcat文檔提供的,與上面Tomcat文檔提供的配置資源是對應的。
獲取資源:
l Context:javax.naming.Context;
l InitialContext:javax.naming.InitialContext;
l lookup(String):獲取資源的方法,其中”java:comp/env”是資源的入口(這是固定的名稱),獲取過來的還是一個Context,這說明需要在獲取到的Context上進一步進行獲取。”bean/MyBeanFactory”對應<Resource>中配置的name值,這回獲取的就是資源對象了。
Context cxt = new InitialContext(); DataSource ds = (DataSource)cxt.lookup("java:/comp/env/mydbcp"); Connection con = ds.getConnection(); System.out.println(con); con.close(); |
Context cxt = new InitialContext(); Context envCxt = (Context)cxt.lookup("java:/comp/env"); DataSource ds = (DataSource)env.lookup("mydbcp"); Connection con = ds.getConnection(); System.out.println(con); con.close(); |
上面兩種方式是相同的效果。
修改JdbcUtils
因為已經學習了連接池,那么JdbcUtils的獲取連接對象的方法也要修改一下了。
JdbcUtils.java
public class JdbcUtils { private static DataSource dataSource = new ComboPooledDataSource(); public static DataSource getDataSource() { return dataSource; } public static Connection getConnection() { try { return dataSource.getConnection(); } catch (Exception e) { throw new RuntimeException(e); } } } |
ThreadLocal
1 ThreadLocal API
ThreadLocal類只有三個方法:
l void set(T value):保存值;
l T get():獲取值;
l void remove():移除值。
2 ThreadLocal的內部是Map
ThreadLocal內部其實是個Map來保存數據。雖然在使用ThreadLocal時只給出了值,沒有給出鍵,其實它內部使用了當前線程做為鍵。
class MyThreadLocal<T> { private Map<Thread,T> map = new HashMap<Thread,T>(); public void set(T value) { map.put(Thread.currentThread(), value); } public void remove() { map.remove(Thread.currentThread()); } public T get() { return map.get(Thread.currentThread()); } } |
BaseServlet
1 BaseServlet的作用
在開始客戶管理系統之前,我們先寫一個工具類:BaseServlet。
我們知道,寫一個項目可能會出現N多個Servlet,而且一般一個Servlet只有一個方法(doGet或doPost),如果項目大一些,那么Servlet的數量就會很驚人。
為了避免Servlet的“膨脹”,我們寫一個BaseServlet。它的作用是讓一個Servlet可以處理多種不同的請求。不同的請求調用Servlet的不同方法。我們寫好了BaseServlet后,讓其他Servlet繼承BaseServlet,例如CustomerServlet繼承BaseServlet,然后在CustomerServlet中提供add()、update()、delete()等方法,每個方法對應不同的請求。
2 BaseServlet分析
我們知道,Servlet中處理請求的方法是service()方法,這說明我們需要讓service()方法去調用其他方法。例如調用add()、mod()、del()、all()等方法!具體調用哪個方法需要在請求中給出方法名稱!然后service()方法通過方法名稱來調用指定的方法。
無論是點擊超鏈接,還是提交表單,請求中必須要有method參數,這個參數的值就是要請求的方法名稱,這樣BaseServlet的service()才能通過方法名稱來調用目標方法。例如某個鏈接如下:
<a href=”/xxx/CustomerServlet?method=add”>添加客戶</a>
3 BaseServlet代碼
public class BaseServlet extends HttpServlet { /* * 它會根據請求中的m,來決定調用本類的哪個方法 */ protected void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); res.setContentType("text/html;charset=utf-8"); // 例如:http://localhost:8080/demo1/xxx?m=add String methodName = req.getParameter("method");// 它是一個方法名稱 // 當沒用指定要調用的方法時,那么默認請求的是execute()方法。 if(methodName == null || methodName.isEmpty()) { methodName = "execute"; } Class c = this.getClass(); try { // 通過方法名稱獲取方法的反射對象 Method m = c.getMethod(methodName, HttpServletRequest.class, HttpServletResponse.class); // 反射方法目標方法,也就是說,如果methodName為add,那么就調用add方法。 String result = (String) m.invoke(this, req, res); // 通過返回值完成請求轉發 if(result != null && !result.isEmpty()) { req.getRequestDispatcher(result).forward(req, res); } } catch (Exception e) { throw new ServletException(e); } } } |
DBUtils
1 DBUtils簡介
DBUtils是Apache Commons組件中的一員,開源免費!
DBUtils是對JDBC的簡單封裝,但是它還是被很多公司使用!
DBUtils的Jar包:dbutils.jar
2 DBUtils主要類
l DbUtils:都是靜態方法,一系列的close()方法;
l QueryRunner:
Ø update():執行insert、update、delete;
Ø query():執行select語句;
Ø batch():執行批處理。
3 QueryRunner之更新
QueryRunner的update()方法可以用來執行insert、update、delete語句。
1. 創建QueryRunner
構造器:QueryRunner();
2. update()方法
int update(Connection con, String sql, Object… params)
@Test public void fun1() throws SQLException { QueryRunner qr = new QueryRunner(); String sql = "insert into user values(?,?,?)"; qr.update(JdbcUtils.getConnection(), sql, "u1", "zhangSan", "123"); } |
還有另一種方式來使用QueryRunner
1. 創建QueryRunner
構造器:QueryRunner(DataSource)
2. update()方法
int update(String sql, Object… params)
這種方式在創建QueryRunner時傳遞了連接池對象,那么在調用update()方法時就不用再傳遞Connection了。
@Test public void fun2() throws SQLException { QueryRunner qr = new QueryRunner(JdbcUtils.getDataSource()); String sql = "insert into user values(?,?,?)"; qr.update(sql, "u1", "zhangSan", "123"); } |
4 ResultSetHandler
我們知道在執行select語句之后得到的是ResultSet,然后我們還需要對ResultSet進行轉換,得到最終我們想要的數據。你可以希望把ResultSet的數據放到一個List中,也可能想把數據放到一個Map中,或是一個Bean中。
DBUtils提供了一個接口ResultSetHandler,它就是用來ResultSet轉換成目標類型的工具。你可以自己去實現這個接口,把ResultSet轉換成你想要的類型。
DBUtils提供了很多個ResultSetHandler接口的實現,這些實現已經基本夠用了,我們通常不用自己去實現ResultSet接口了。
l MapHandler:單行處理器!把結果集轉換成Map<String,Object>,其中列名為鍵!
l MapListHandler:多行處理器!把結果集轉換成List<Map<String,Object>>;
l BeanHandler:單行處理器!把結果集轉換成Bean,該處理器需要Class參數,即Bean的類型;
l BeanListHandler:多行處理器!把結果集轉換成List<Bean>;
l ColumnListHandler:多行單列處理器!把結果集轉換成List<Object>,使用ColumnListHandler時需要指定某一列的名稱或編號,例如:new ColumListHandler(“name”)表示把name列的數據放到List中。
l ScalarHandler:單行單列處理器!把結果集轉換成Object。一般用於聚集查詢,例如select count(*) from tab_student。
Map處理器
Bean處理器
Column處理器
Scalar處理器
5 QueryRunner之查詢
QueryRunner的查詢方法是:
public <T> T query(String sql, ResultSetHandler<T> rh, Object… params)
public <T> T query(Connection con, String sql, ResultSetHandler<T> rh, Object… params)
query()方法會通過sql語句和params查詢出ResultSet,然后通過rh把ResultSet轉換成對應的類型再返回。
@Test public void fun1() throws SQLException { DataSource ds = JdbcUtils.getDataSource(); QueryRunner qr = new QueryRunner(ds); String sql = "select * from tab_student where number=?"; Map<String,Object> map = qr.query(sql, new MapHandler(), "S_2000"); System.out.println(map); } @Test public void fun2() throws SQLException { DataSource ds = JdbcUtils.getDataSource(); QueryRunner qr = new QueryRunner(ds); String sql = "select * from tab_student"; List<Map<String,Object>> list = qr.query(sql, new MapListHandler()); for(Map<String,Object> map : list) { System.out.println(map); } } @Test public void fun3() throws SQLException { DataSource ds = JdbcUtils.getDataSource(); QueryRunner qr = new QueryRunner(ds); String sql = "select * from tab_student where number=?"; Student stu = qr.query(sql, new BeanHandler<Student>(Student.class), "S_2000"); System.out.println(stu); } @Test public void fun4() throws SQLException { DataSource ds = JdbcUtils.getDataSource(); QueryRunner qr = new QueryRunner(ds); String sql = "select * from tab_student"; List<Student> list = qr.query(sql, new BeanListHandler<Student>(Student.class)); for(Student stu : list) { System.out.println(stu); } } @Test public void fun5() throws SQLException { DataSource ds = JdbcUtils.getDataSource(); QueryRunner qr = new QueryRunner(ds); String sql = "select * from tab_student"; List<Object> list = qr.query(sql, new ColumnListHandler("name")); for(Object s : list) { System.out.println(s); } } @Test public void fun6() throws SQLException { DataSource ds = JdbcUtils.getDataSource(); QueryRunner qr = new QueryRunner(ds); String sql = "select count(*) from tab_student"; Number number = (Number)qr.query(sql, new ScalarHandler()); int cnt = number.intValue(); System.out.println(cnt); } |
6 QueryRunner之批處理
QueryRunner還提供了批處理方法:batch()。
我們更新一行記錄時需要指定一個Object[]為參數,如果是批處理,那么就要指定Object[][]為參數了。即多個Object[]就是Object[][]了,其中每個Object[]對應一行記錄:
@Test public void fun10() throws SQLException { DataSource ds = JdbcUtils.getDataSource(); QueryRunner qr = new QueryRunner(ds); String sql = "insert into tab_student values(?,?,?,?)"; Object[][] params = new Object[10][];//表示 要插入10行記錄 for(int i = 0; i < params.length; i++) { params[i] = new Object[]{"S_300" + i, "name" + i, 30 + i, i%2==0?"男":"女"}; } qr.batch(sql, params); } |
過濾器(Filter)
過濾器概述
1 什么是過濾器
過濾器JavaWeb三大組件之一,它與Servlet很相似!不它過濾器是用來攔截請求的,而不是處理請求的。
當用戶請求某個Servlet時,會先執行部署在這個請求上的Filter,如果Filter“放行”,那么會繼承執行用戶請求的Servlet;如果Filter不“放行”,那么就不會執行用戶請求的Servlet。
其實可以這樣理解,當用戶請求某個Servlet時,Tomcat會去執行注冊在這個請求上的Filter,然后是否“放行”由Filter來決定。可以理解為,Filter來決定是否調用Servlet!當執行完成Servlet的代碼后,還會執行Filter后面的代碼。
2 過濾器之hello world
其實過濾器與Servlet很相似,我們回憶一下如果寫的第一個Servlet應用!寫一個類,實現Servlet接口!沒錯,寫過濾器就是寫一個類,實現Filter接口。
public class HelloFilter implements Filter { public void init(FilterConfig filterConfig) throws ServletException {} public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println("Hello Filter"); } public void destroy() {} } |
第二步也與Servlet一樣,在web.xml文件中部署Filter:
<filter> <filter-name>helloFilter</filter-name> <filter-class>cn.itcast.filter.HelloFilter</filter-class> </filter> <filter-mapping> <filter-name>helloFilter</filter-name> <url-pattern>/index.jsp</url-pattern> </filter-mapping> |
應該沒有問題吧,都可以看懂吧!
OK了,現在可以嘗試去訪問index.jsp頁面了,看看是什么效果!
當用戶訪問index.jsp頁面時,會執行HelloFilter的doFilter()方法!在我們的示例中,index.jsp頁面是不會被執行的,如果想執行index.jsp頁面,那么我們需要放行!
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println("filter start..."); chain.doFilter(request, response); System.out.println("filter end..."); } |
有很多同學總是錯誤的認為,一個請求在給客戶端輸出之后就算是結束了,這是不對的!其實很多事情都需要在給客戶端響應之后才能完成!
過濾器詳細
1 過濾器的生命周期
我們已經學習過Servlet的生命周期,那么Filter的生命周期也就沒有什么難度了!
l init(FilterConfig):在服務器啟動時會創建Filter實例,並且每個類型的Filter只創建一個實例,從此不再創建!在創建完Filter實例后,會馬上調用init()方法完成初始化工作,這個方法只會被執行一次;
l doFilter(ServletRequest req,ServletResponse res,FilterChain chain):這個方法會在用戶每次訪問“目標資源(<url->pattern>index.jsp</url-pattern>)”時執行,如果需要“放行”,那么需要調用FilterChain的doFilter(ServletRequest,ServletResponse)方法,如果不調用FilterChain的doFilter()方法,那么目標資源將無法執行;
l destroy():服務器會在創建Filter對象之后,把Filter放到緩存中一直使用,通常不會銷毀它。一般會在服務器關閉時銷毀Filter對象,在銷毀Filter對象之前,服務器會調用Filter對象的destory()方法。
2 FilterConfig
你已經看到了吧,Filter接口中的init()方法的參數類型為FilterConfig類型。它的功能與ServletConfig相似,與web.xml文件中的配置信息對應。下面是FilterConfig的功能介紹:
l ServletContext getServletContext():獲取ServletContext的方法;
l String getFilterName():獲取Filter的配置名稱;與<filter-name>元素對應;
l String getInitParameter(String name):獲取Filter的初始化配置,與<init-param>元素對應;
l Enumeration getInitParameterNames():獲取所有初始化參數的名稱。
3 FilterChain
doFilter()方法的參數中有一個類型為FilterChain的參數,它只有一個方法:doFilter(ServletRequest,ServletResponse)。
前面我們說doFilter()方法的放行,讓請求流訪問目標資源!但這么說不嚴密,其實調用該方法的意思是,“我(當前Filter)”放行了,但不代表其他人(其他過濾器)也放行。
也就是說,一個目標資源上,可能部署了多個過濾器,就好比在你去北京的路上有多個打劫的匪人(過濾器),而其中第一伙匪人放行了,但不代表第二伙匪人也放行了,所以調用FilterChain類的doFilter()方法表示的是執行下一個過濾器的doFilter()方法,或者是執行目標資源!
如果當前過濾器是最后一個過濾器,那么調用chain.doFilter()方法表示執行目標資源,而不是最后一個過濾器,那么chain.doFilter()表示執行下一個過濾器的doFilter()方法。
4 多個過濾器執行順序
一個目標資源可以指定多個過濾器,過濾器的執行順序是在web.xml文件中的部署順序:
<filter> <filter-name>myFilter1</filter-name> <filter-class>cn.itcast.filter.MyFilter1</filter-class> </filter> <filter-mapping> <filter-name>myFilter1</filter-name> <url-pattern>/index.jsp</url-pattern> </filter-mapping> <filter> <filter-name>myFilter2</filter-name> <filter-class>cn.itcast.filter.MyFilter2</filter-class> </filter> <filter-mapping> <filter-name>myFilter2</filter-name> <url-pattern>/index.jsp</url-pattern> </filter-mapping> |
public class MyFilter1 extends HttpFilter { public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println("filter1 start..."); chain.doFilter(request, response);//放行,執行MyFilter2的doFilter()方法 System.out.println("filter1 end..."); } } |
public class MyFilter2 extends HttpFilter { public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println("filter2 start..."); chain.doFilter(request, response);//放行,執行目標資源 System.out.println("filter2 end..."); } } |
<body> This is my JSP page. <br> <h1>index.jsp</h1> <%System.out.println("index.jsp"); %> </body> |
當有用戶訪問index.jsp頁面時,輸出結果如下:
filter1 start... filter2 start... index.jsp filter2 end... filter1 end... |
5 四種攔截方式
我們來做個測試,寫一個過濾器,指定過濾的資源為b.jsp,然后我們在瀏覽器中直接訪問b.jsp,你會發現過濾器執行了!
但是,當我們在a.jsp中request.getRequestDispathcer(“/b.jsp”).forward(request,response)時,就不會再執行過濾器了!也就是說,默認情況下,只能直接訪問目標資源才會執行過濾器,而forward執行目標資源,不會執行過濾器!
public class MyFilter extends HttpFilter { public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println("myfilter..."); chain.doFilter(request, response); } } |
<filter> <filter-name>myfilter</filter-name> <filter-class>cn.itcast.filter.MyFilter</filter-class> </filter> <filter-mapping> <filter-name>myfilter</filter-name> <url-pattern>/b.jsp</url-pattern> </filter-mapping> |
<body> <h1>b.jsp</h1> </body> |
<h1>a.jsp</h1> <% request.getRequestDispatcher("/b.jsp").forward(request, response); %> </body> |
http://localhost:8080/filtertest/b.jsp -->直接訪問b.jsp時,會執行過濾器內容;
http://localhost:8080/filtertest/a.jsp --> 訪問a.jsp,但a.jsp會forward到b.jsp,這時就不會執行過濾器!
其實過濾器有四種攔截方式!分別是:REQUEST、FORWARD、INCLUDE、ERROR。
l REQUEST:直接訪問目標資源時執行過濾器。包括:在地址欄中直接訪問、表單提交、超鏈接、重定向,只要在地址欄中可以看到目標資源的路徑,就是REQUEST;
l FORWARD:轉發訪問執行過濾器。包括RequestDispatcher#forward()方法、<jsp:forward>標簽都是轉發訪問;
l INCLUDE:包含訪問執行過濾器。包括RequestDispatcher#include()方法、<jsp:include>標簽都是包含訪問;
l ERROR:當目標資源在web.xml中配置為<error-page>中時,並且真的出現了異常,轉發到目標資源時,會執行過濾器。
可以在<filter-mapping>中添加0~n個<dispatcher>子元素,來說明當前訪問的攔截方式。
<filter-mapping> <filter-name>myfilter</filter-name> <url-pattern>/b.jsp</url-pattern> <dispatcher>REQUEST</dispatcher> <dispatcher>FORWARD</dispatcher> </filter-mapping> |
<filter-mapping> <filter-name>myfilter</filter-name> <url-pattern>/b.jsp</url-pattern> </filter-mapping> |
<filter-mapping> <filter-name>myfilter</filter-name> <url-pattern>/b.jsp</url-pattern> <dispatcher>FORWARD</dispatcher> </filter-mapping> |
其實最為常用的就是REQUEST和FORWARD兩種攔截方式,而INCLUDE和ERROR都比較少用!其中INCLUDE比較好理解,我們這里不再給出代碼,學員可以通過FORWARD方式修改,來自己測試。而ERROR方式不易理解,下面給出ERROR攔截方式的例子:
<filter-mapping> <filter-name>myfilter</filter-name> <url-pattern>/b.jsp</url-pattern> <dispatcher>ERROR</dispatcher> </filter-mapping> <error-page> <error-code>500</error-code> <location>/b.jsp</location> </error-page> |
<body> <h1>a.jsp</h1> <% if(true) throw new RuntimeException("嘻嘻~"); %> </body> |
6 過濾器的應用場景
過濾器的應用場景:
l 執行目標資源之前做預處理工作,例如設置編碼,這種試通常都會放行,只是在目標資源執行之前做一些准備工作;
l 通過條件判斷是否放行,例如校驗當前用戶是否已經登錄,或者用戶IP是否已經被禁用;
l 在目標資源執行后,做一些后續的特殊處理工作,例如把目標資源輸出的數據進行處理;
7 設置目標資源
在web.xml文件中部署Filter時,可以通過“*”來執行目標資源:
<filter-mapping> <filter-name>myfilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> |
這一特性與Servlet完全相同!通過這一特性,我們可以在用戶訪問敏感資源時,執行過濾器,例如:<url-pattern>/admin/*<url-pattern>,可以把所有管理員才能訪問的資源放到/admin路徑下,這時可以通過過濾器來校驗用戶身份。
還可以為<filter-mapping>指定目標資源為某個Servlet,例如:
<servlet> <servlet-name>myservlet</servlet-name> <servlet-class>cn.itcast.servlet.MyServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>myservlet</servlet-name> <url-pattern>/abc</url-pattern> </servlet-mapping> <filter> <filter-name>myfilter</filter-name> <filter-class>cn.itcast.filter.MyFilter</filter-class> </filter> <filter-mapping> <filter-name>myfilter</filter-name> <servlet-name>myservlet</servlet-name> </filter-mapping> |
當用戶訪問http://localhost:8080/filtertest/abc時,會執行名字為myservlet的Servlet,這時會執行過濾器。
8 Filter小結
Filter的三個方法:
l void init(FilterConfig):在Tomcat啟動時被調用;
l void destroy():在Tomcat關閉時被調用;
l void doFilter(ServletRequest,ServletResponse,FilterChain):每次有請求時都調用該方法;
FilterConfig類:與ServletConfig相似,用來獲取Filter的初始化參數
l ServletContext getServletContext():獲取ServletContext的方法;
l String getFilterName():獲取Filter的配置名稱;
l String getInitParameter(String name):獲取Filter的初始化配置,與<init-param>元素對應;
l Enumeration getInitParameterNames():獲取所有初始化參數的名稱。
FilterChain類:
l void doFilter(ServletRequest,ServletResponse):放行!表示執行下一個過濾器,或者執行目標資源。可以在調用FilterChain的doFilter()方法的前后添加語句,在FilterChain的doFilter()方法之前的語句會在目標資源執行之前執行,在FilterChain的doFilter()方法之后的語句會在目標資源執行之后執行。
四各攔截方式:REQUEST、FORWARD、INCLUDE、ERROR,默認是REQUEST方式。
l REQUEST:攔截直接請求方式;
l FORWARD:攔截請求轉發方式;
l INCLUDE:攔截請求包含方式;
l ERROR:攔截錯誤轉發方式。
過濾器應用案例
分ip統計網站的訪問次數
1 說明
網站統計每個IP地址訪問本網站的次數。
2 分析
因為一個網站可能有多個頁面,無論哪個頁面被訪問,都要統計訪問次數,所以使用過濾器最為方便。
因為需要分IP統計,所以可以在過濾器中創建一個Map,使用IP為key,訪問次數為value。當有用戶訪問時,獲取請求的IP,如果IP在Map中存在,說明以前訪問過,那么在訪問次數上加1,即可;IP在Map中不存在,那么設置次數為1。
把這個Map存放到ServletContext中!
3 代碼
index.jsp
<body> <h1>分IP統計訪問次數</h1> <table align="center" width="50%" border="1"> <tr> <th>IP地址</th> <th>次數</th> </tr> <c:forEach items="${applicationScope.ipCountMap }" var="entry"> <tr> <td>${entry.key }</td> <td>${entry.value }</td> </tr> </c:forEach> </table> </body> |
IPFilter
public class IPFilter implements Filter { private ServletContext context; public void init(FilterConfig fConfig) throws ServletException { context = fConfig.getServletContext(); Map<String, Integer> ipCountMap = Collections .synchronizedMap(new LinkedHashMap<String, Integer>()); context.setAttribute("ipCountMap", ipCountMap); } @SuppressWarnings("unchecked") public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; String ip = req.getRemoteAddr(); Map<String, Integer> ipCountMap = (Map<String, Integer>) context .getAttribute("ipCountMap"); Integer count = ipCountMap.get(ip); if (count == null) { count = 1; } else { count += 1; } ipCountMap.put(ip, count); context.setAttribute("ipCountMap", ipCountMap); chain.doFilter(request, response); } public void destroy() {} } |
<filter> <display-name>IPFilter</display-name> <filter-name>IPFilter</filter-name> <filter-class>cn.itcast.filter.ip.IPFilter</filter-class> </filter> <filter-mapping> <filter-name>IPFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> |
粗粒度權限控制(攔截是否登錄、攔截用戶名admin權限)
1 說明
我們給出三個頁面:index.jsp、user.jsp、admin.jsp。
l index.jsp:誰都可以訪問,沒有限制;
l user.jsp:只有登錄用戶才能訪問;
l admin.jsp:只有管理員才能訪問。
2 分析
設計User類:username、password、grade,其中grade表示用戶等級,1表示普通用戶,2表示管理員用戶。
當用戶登錄成功后,把user保存到session中。
創建LoginFilter,它有兩種過濾方式:
l 如果訪問的是user.jsp,查看session中是否存在user;
l 如果訪問的是admin.jsp,查看session中是否存在user,並且user的grade等於2。
3 代碼
User.java
public class User { private String username; private String password; private int grade; … } |
為了方便,這里就不使用數據庫了,所以我們需要在UserService中創建一個Map,用來保存所有用戶。Map中的key中用戶名,value為User對象。
UserService.java
public class UserService { private static Map<String,User> users = new HashMap<String,User>(); static { users.put("zhangSan", new User("zhangSan", "123", 1)); users.put("liSi", new User("liSi", "123", 2)); } public User login(String username, String password) { User user = users.get(username); if(user == null) return null; return user.getPassword().equals(password) ? user : null; } } |
login.jsp
<body> <h1>登錄</h1> <p style="font-weight: 900; color: red">${msg }</p> <form action="<c:url value='/LoginServlet'/>" method="post"> 用戶名:<input type="text" name="username"/><br/> 密 碼:<input type="password" name="password"/><br/> <input type="submit" value="登錄"/> </form> </body> |
index.jsp
<body> <h1>主頁</h1> <h3>${user.username }</h3> <hr/> <a href="<c:url value='/login.jsp'/>">登錄</a><br/> <a href="<c:url value='/user/user.jsp'/>">用戶頁面</a><br/> <a href="<c:url value='/admin/admin.jsp'/>">管理員頁面</a> </body> |
/user/user.jsp
<body> <h1>用戶頁面</h1> <h3>${user.username }</h3> <hr/> </body> |
/admin/admin.jsp
<body> <h1>管理員頁面</h1> <h3>${user.username }</h3> <hr/> </body> |
LoginServlet
public class LoginServlet extends HttpServlet { public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("utf-8"); response.setContentType("text/html;charset=utf-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); UserService userService = new UserService(); User user = userService.login(username, password); if(user == null) { request.setAttribute("msg", "用戶名或密碼錯誤"); request.getRequestDispatcher("/login.jsp").forward(request, response); } else { request.getSession().setAttribute("user", user); request.getRequestDispatcher("/index.jsp").forward(request, response); } } } |
LoginUserFilter.java
<filter> <display-name>LoginUserFilter</display-name> <filter-name>LoginUserFilter</filter-name> <filter-class>cn.itcast.filter.LoginUserFilter</filter-class> </filter> <filter-mapping> <filter-name>LoginUserFilter</filter-name> <url-pattern>/user/*</url-pattern> </filter-mapping> |
public class LoginUserFilter implements Filter { public void destroy() {} public void init(FilterConfig fConfig) throws ServletException {} public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { response.setContentType("text/html;charset=utf-8"); HttpServletRequest req = (HttpServletRequest) request; User user = (User) req.getSession().getAttribute("user"); if(user == null) { response.getWriter().print("您還沒有登錄"); return; } chain.doFilter(request, response); } } |
LoginAdminFilter.java
<filter> <display-name>LoginAdminFilter</display-name> <filter-name>LoginAdminFilter</filter-name> <filter-class>cn.itcast.filter.LoginAdminFilter</filter-class> </filter> <filter-mapping> <filter-name>LoginAdminFilter</filter-name> <url-pattern>/admin/*</url-pattern> </filter-mapping> |
public class LoginAdminFilter implements Filter { public void destroy() {} public void init(FilterConfig fConfig) throws ServletException {} public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { response.setContentType("text/html;charset=utf-8"); HttpServletRequest req = (HttpServletRequest) request; User user = (User) req.getSession().getAttribute("user"); if(user == null) { response.getWriter().print("您還沒有登錄!"); return; } if(user.getGrade() < 2) { response.getWriter().print("您的等級不夠!"); return; } chain.doFilter(request, response); } } |
禁用資源緩存
瀏覽器只是要緩存頁面,這對我們在開發時測試很不方便,所以我們可以過濾所有資源,然后添加去除所有緩存!
public class NoCacheFilter extends HttpFilter { public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { response.setHeader("cache-control", "no-cache"); response.setHeader("pragma", "no-cache"); response.setHeader("expires", "0"); chain.doFilter(request, response); } } |
但是要注意,有的瀏覽器可能不會理會你的設置,還是會緩存的!這時就要在頁面中使用時間戳來處理了。
解決全站字符亂碼(POST和GET中文編碼問題)
1 說明
亂碼問題:
l 獲取請求參數中的亂碼問題;
Ø POST請求:request.setCharacterEncoding(“utf-8”);
Ø GET請求:new String(request.getParameter(“xxx”).getBytes(“iso-8859-1”), “utf-8”);
l 響應的亂碼問題:response.setContextType(“text/html;charset=utf-8”)。
基本上在每個Servlet中都要處理亂碼問題,所以應該把這個工作放到過濾器中來完成。
2 分析
其實全站亂碼問題的難點就是處理GET請求參數的問題。
如果只是處理POST請求的編碼問題,以及響應編碼問題,那么這個過濾器就太!太!太簡單的。
public class EncodingFilter extends HttpFilter { public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String charset = this.getInitParameter("charset"); if(charset == null || charset.isEmpty()) { charset = "UTF-8"; } request.setCharacterEncoding(charset); response.setContentType("text/html;charset=" + charset); chain.doFilter(request, response); } } |
如果是POST請求,當執行目標Servlet時,Servlet中調用request.getParameter()方法時,就會根據request.setCharacterEncoding()設置的編碼來轉碼!這說明在過濾器中調用request.setCharacterEncoding()方法會影響在目標Servlet中的request.getParameter()方法的行為!
但是如果是GET請求,我們又如何能影響request.getParameter()方法的行為呢?這是不好做到的!我們不可能先調用request.getParameter()方法獲取參數,然后手動轉碼后,再施加在到request中!因為request只有getParameter(),而沒有setParameter()方法。
處理GET請求參數編碼問題,需要在Filter中放行時,把request對象給“調包”了,也就是讓目標Servlet使用我們“調包”之后的request對象。這說明我們需要保證“調包”之后的request對象中所有方法都要與“調包”之前一樣可以使用,並且getParameter()方法還要有能力返回轉碼之后的參數。
這可能讓你想起了“繼承”,但是這里不能用繼承,而是“裝飾者模式(Decorator Pattern)”!
下面是三種對a對象進行增強的手段:
l 繼承:AA類繼承a對象的類型:A類,然后重寫fun1()方法,其中重寫的fun1()方法就是被增強的方法。但是,繼承必須要知道a對象的真實類型,然后才能去繼承。如果我們不知道a對象的確切類型,而只知道a對象是IA接口的實現類對象,那么就無法使用繼承來增強a對象了;
l 裝飾者模式:AA類去實現a對象相同的接口:IA接口,還需要給AA類傳遞a對象,然后在AA類中所有的方法實現都是通過代理a對象的相同方法完成的,只有fun1()方法在代理a對象相同方法的前后添加了一些內容,這就是對fun1()方法進行了增強;
l 動態代理:動態代理與裝飾者模式比較相似,而且是通過反射來完成的。動態代理會在最后一天的基礎加強中講解,這里就不再廢話了。
對request對象進行增強的條件,剛好符合裝飾者模式的特點!因為我們不知道request對象的具體類型,但我們知道request是HttpServletRequest接口的實現類。這說明我們寫一個類EncodingRequest,去實現HttpServletRequest接口,然后再把原來的request傳遞給EncodingRequest類!在EncodingRequest中對HttpServletRequest接口中的所有方法的實現都是通過代理原來的request對象來完成的,只有對getParameter()方法添加了增強代碼!
JavaEE已經給我們提供了一個HttpServletRequestWrapper類,它就是HttpServletRequest的包裝類,但它做任何的增強!你可能會說,寫一個裝飾類,但不做增強,其目的是什么呢?使用這個裝飾類的對象,和使用原有的request有什么分別呢?
HttpServletRequestWrapper類雖然是HttpServletRequest的裝飾類,但它不是用來直接使用的,而是用來讓我們去繼承的!當我們想寫一個裝飾類時,還要對所有不需要增強的方法做一次實現是很心煩的事情,但如果你去繼承HttpServletRequestWrapper類,那么就只需要重寫需要增強的方法即可了。
3 代碼
EncodingRequest
public class EncodingRequest extends HttpServletRequestWrapper { private String charset; public EncodingRequest(HttpServletRequest request, String charset) { super(request); this.charset = charset; } public String getParameter(String name) { HttpServletRequest request = (HttpServletRequest) getRequest(); String method = request.getMethod(); if(method.equalsIgnoreCase("post")) { try { request.setCharacterEncoding(charset); } catch (UnsupportedEncodingException e) {} } else if(method.equalsIgnoreCase("get")) { String value = request.getParameter(name); try { value = new String(name.getBytes("ISO-8859-1"), charset); } catch (UnsupportedEncodingException e) { } return value; } return request.getParameter(name); } } |
EncodingFilter
public class EncodingFilter extends HttpFilter { public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String charset = this.getInitParameter("charset"); if(charset == null || charset.isEmpty()) { charset = "UTF-8"; } response.setCharacterEncoding(charset); response.setContentType("text/html;charset=" + charset); EncodingRequest res = new EncodingRequest(request, charset); chain.doFilter(res, response); } } |
web.xml
<filter> <filter-name>EncodingFilter</filter-name> <filter-class>cn.itcast.filter.EncodingFilter</filter-class> <init-param> <param-name>charset</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>EncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> |
文件上傳下載
文件上傳概述
1 文件上傳的作用
例如網絡硬盤!就是用來上傳下載文件的。
在智聯招聘上填寫一個完整的簡歷還需要上傳照片呢。
2 文件上傳對頁面的要求
上傳文件的要求比較多,需要記一下:
1. 必須使用表單,而不能是超鏈接;
2. 表單的method必須是POST,而不能是GET;
3. 表單的enctype必須是multipart/form-data;
4. 在表單中添加file表單字段,即<input type=”file”…/>
<form action="${pageContext.request.contextPath }/FileUploadServlet" method="post"enctype="multipart/form-data"> 用戶名:<input type="text" name="username"/><br/> 文件1:<input type="file" name="file1"/><br/> 文件2:<input type="file" name="file2"/><br/> <input type="submit" value="提交"/> </form> |
3 比對文件上傳表單和普通文本表單的區別
通過httpWatch查看“文件上傳表單”和“普通文本表單”的區別。
l 文件上傳表單的enctype=”multipart/form-data”,表示多部件表單數據;
l 普通文本表單可以不設置enctype屬性:
Ø 當method=”post”時,enctype的默認值為application/x-www-form-urlencoded,表示使用url編碼正文;
Ø 當method=”get”時,enctype的默認值為null,沒有正文,所以就不需要enctype了。
對普通文本表單的測試:
<form action="${pageContext.request.contextPath }/FileUploadServlet" method="post"> 用戶名:<input type="text" name="username"/><br/> 文件1:<input type="file" name="file1"/><br/> 文件2:<input type="file" name="file2"/><br/> <input type="submit" value="提交"/> </form> |
通過httpWatch測試,查看表單的請求數據正文,我們發現請求中只有文件名稱,而沒有文件內容。也就是說,當表單的enctype不是multipart/form-data時,請求中不包含文件內容,而只有文件的名稱,這說明普通文本表單中input:file與input:text沒什么區別了。
對文件上傳表單的測試:
<form action="${pageContext.request.contextPath }/FileUploadServlet" method="post"enctype="multipart/form-data"> 用戶名:<input type="text" name="username"/><br/> 文件1:<input type="file" name="file1"/><br/> 文件2:<input type="file" name="file2"/><br/> <input type="submit" value="提交"/> </form> |
通過httpWatch測試,查看表單的請求數據正文部分,發現正文部分是由多個部件組成,每個部件對應一個表單字段,每個部件都有自己的頭信息。頭信息下面是空行,空行下面是字段的正文部分。多個部件之間使用隨機生成的分隔線隔開。
文本字段的頭信息中只包含一條頭信息,即Content-Disposition,這個頭信息的值有兩個部分,第一部分是固定的,即form-data,第二部分為字段的名稱。在空行后面就是正文部分了,正文部分就是在文本框中填寫的內容。
文件字段的頭信息中包含兩條頭信息,Content-Disposition和Content-Type。Content-Disposition中多出一個filename,它指定的是上傳的文件名稱。而Content-Type指定的是上傳文件的類型。文件字段的正文部分就是文件的內容。
請注意,因為我們上傳的文件都是普通文本文件,即txt文件,所以在httpWatch中是可以正常顯示的,如果上傳的是exe、mp3等文件,那么在httpWatch看到的就是亂碼了。
4 文件上傳對Servlet的要求
當提交的表單是文件上傳表單時,那么對Servlet也是有要求的。
首先我們要肯定一點,文件上傳表單的數據也是被封裝到request對象中的。
request.getParameter(String)方法獲取指定的表單字段字符內容,但文件上傳表單已經不在是字符內容,而是字節內容,所以失效。
這時可以使用request的getInputStream()方法獲取ServletInputStream對象,它是InputStream的子類,這個ServletInputStream對象對應整個表單的正文部分(從第一個分隔線開始,到最后),這說明我們需要的解析流中的數據。當然解析它是很麻煩的一件事情,而Apache已經幫我們提供了解析它的工具:commons-fileupload。
可以嘗試把request.getInputStream()這個流中的內容打印出來,再對比httpWatch中的請求數據。
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { InputStream in = request.getInputStream(); String s = IOUtils.toString(in); System.out.println(s); } |
-----------------------------7ddd3370ab2 Content-Disposition: form-data; name="username" hello -----------------------------7ddd3370ab2 Content-Disposition: form-data; name="file1"; filename="a.txt" Content-Type: text/plain aaa -----------------------------7ddd3370ab2 Content-Disposition: form-data; name="file2"; filename="b.txt" Content-Type: text/plain bbb -----------------------------7ddd3370ab2-- |
commons-fileupload
為什么使用fileupload:
上傳文件的要求比較多,需要記一下:
l 必須是POST表單;
l 表單的enctype必須是multipart/form-data;
l 在表單中添加file表單字段,即<input type=”file”…/>
Servlet的要求:
l 不能再使用request.getParameter()來獲取表單數據;
l 可以使用request.getInputStream()得到所有的表單數據,而不是一個表單項的數據;
l 這說明不使用fileupload,我們需要自己來對request.getInputStream()的內容進行解析!!!
1 fileupload概述
fileupload是由apache的commons組件提供的上傳組件。它最主要的工作就是幫我們解析request.getInputStream()。
fileupload組件需要的JAR包有:
l commons-fileupload.jar,核心包;
l commons-io.jar,依賴包。
2 fileupload簡單應用
fileupload的核心類有:DiskFileItemFactory、ServletFileUpload、FileItem。
使用fileupload組件的步驟如下:
1. 創建工廠類DiskFileItemFactory對象:DiskFileItemFactory factory = new DiskFileItemFactory()
2. 使用工廠創建解析器對象:ServletFileUpload fileUpload = new ServletFileUpload(factory)
3. 使用解析器來解析request對象:List<FileItem> list = fileUpload.parseRequest(request)
隆重介紹FileItem類,它才是我們最終要的結果。一個FileItem對象對應一個表單項(表單字段)。一個表單中存在文件字段和普通字段,可以使用FileItem類的isFormField()方法來判斷表單字段是否為普通字段,如果不是普通字段,那么就是文件字段了。
l String getName():獲取文件字段的文件名稱;
l String getString():獲取字段的內容,如果是文件字段,那么獲取的是文件內容,當然上傳的文件必須是文本文件;
l String getFieldName():獲取字段名稱,例如:<input type=”text” name=”username”/>,返回的是username;
l String getContentType():獲取上傳的文件的類型,例如:text/plain。
l int getSize():獲取上傳文件的大小;
l boolean isFormField():判斷當前表單字段是否為普通文本字段,如果返回false,說明是文件字段;
l InputStream getInputStream():獲取上傳文件對應的輸入流;
l void write(File):把上傳的文件保存到指定文件中。
3 簡單上傳示例
寫一個簡單的上傳示例:
l 表單包含一個用戶名字段,以及一個文件字段;
l Servlet保存上傳的文件到uploads目錄,顯示用戶名,文件名,文件大小,文件類型。
第一步:
完成index.jsp,只需要一個表單。注意表單必須是post的,而且enctype必須是mulitpart/form-data的。
<form action="${pageContext.request.contextPath }/FileUploadServlet" method="post"enctype="multipart/form-data"> 用戶名:<input type="text" name="username"/><br/> 文件1:<input type="file" name="file1"/><br/> <input type="submit" value="提交"/> </form> |
第二步:
完成FileUploadServlet
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 因為要使用response打印,所以設置其編碼 response.setContentType("text/html;charset=utf-8"); // 創建工廠 DiskFileItemFactory dfif = new DiskFileItemFactory(); // 使用工廠創建解析器對象 ServletFileUpload fileUpload = new ServletFileUpload(dfif); try { // 使用解析器對象解析request,得到FileItem列表 List<FileItem> list = fileUpload.parseRequest(request); // 遍歷所有表單項 for(FileItem fileItem : list) { // 如果當前表單項為普通表單項 if(fileItem.isFormField()) { // 獲取當前表單項的字段名稱 String fieldName = fileItem.getFieldName(); // 如果當前表單項的字段名為username if(fieldName.equals("username")) { // 打印當前表單項的內容,即用戶在username表單項中輸入的內容 response.getWriter().print("用戶名:" + fileItem.getString() + "<br/>"); } } else {//如果當前表單項不是普通表單項,說明就是文件字段 String name = fileItem.getName();//獲取上傳文件的名稱 // 如果上傳的文件名稱為空,即沒有指定上傳文件 if(name == null || name.isEmpty()) { continue; } // 獲取真實路徑,對應${項目目錄}/uploads,當然,這個目錄必須存在 String savepath = this.getServletContext().getRealPath("/uploads"); // 通過uploads目錄和文件名稱來創建File對象 File file = new File(savepath, name); // 把上傳文件保存到指定位置 fileItem.write(file); // 打印上傳文件的名稱 response.getWriter().print("上傳文件名:" + name + "<br/>"); // 打印上傳文件的大小 response.getWriter().print("上傳文件大小:" + fileItem.getSize() + "<br/>"); // 打印上傳文件的類型 response.getWriter().print("上傳文件類型:" + fileItem.getContentType() + "<br/>"); } } } catch (Exception e) { throw new ServletException(e); } } |
文件上傳之細節
1 把上傳的文件放到WEB-INF目錄下
如果沒有把用戶上傳的文件存放到WEB-INF目錄下,那么用戶就可以通過瀏覽器直接訪問上傳的文件,這是非常危險的。
假如說用戶上傳了一個a.jsp文件,然后用戶在通過瀏覽器去訪問這個a.jsp文件,那么就會執行a.jsp中的內容,如果在a.jsp中有如下語句:Runtime.getRuntime().exec(“shutdown –s –t 1”);,那么你就會…
通常我們會在WEB-INF目錄下創建一個uploads目錄來存放上傳的文件,而在Servlet中找到這個目錄需要使用ServletContext的getRealPath(String)方法,例如在我的upload1項目中有如下語句:
ServletContext servletContext = this.getServletContext();
String savepath = servletContext.getRealPath(“/WEB-INF/uploads”);
其中savepath為:F:\tomcat6_1\webapps\upload1\WEB-INF\uploads。
2 文件名稱(完整路徑、文件名稱)
上傳文件名稱可能是完整路徑:
IE6獲取的上傳文件名稱是完整路徑,而其他瀏覽器獲取的上傳文件名稱只是文件名稱而已。瀏覽器差異的問題我們還是需要處理一下的。
String name = file1FileItem.getName(); response.getWriter().print(name); |
使用不同瀏覽器測試,其中IE6就會返回上傳文件的完整路徑,不知道IE6在搞什么,這給我們帶來了很大的麻煩,就是需要處理這一問題。
處理這一問題也很簡單,無論是否為完整路徑,我們都去截取最后一個“\\”后面的內容就可以了。
String name = file1FileItem.getName(); int lastIndex = name.lastIndexOf("\\");//獲取最后一個“\”的位置 if(lastIndex != -1) {//注意,如果不是完整路徑,那么就不會有“\”的存在。 name = name.substring(lastIndex + 1);//獲取文件名稱 } response.getWriter().print(name); |
3 中文亂碼問題
上傳文件名稱中包含中文:
當上傳的誰的名稱中包含中文時,需要設置編碼,commons-fileupload組件為我們提供了兩種設置編碼的方式:
l request.setCharacterEncoding(String):這種方式是我們最為熟悉的方式了;
l fileUpload.setHeaderEncdoing(String):這種方式的優先級高與前一種。
上傳文件的文件內容包含中文:
通常我們不需關心上傳文件的內容,因為我們會把上傳文件保存到硬盤上!也就是說,文件原來是什么樣子,到服務器這邊還是什么樣子!
但是如果你有這樣的需求,非要在控制台顯示上傳的文件內容,那么你可以使用fileItem.getString(“utf-8”)來處理編碼。
文本文件內容和普通表單項內容使用FileItem類的getString(“utf-8”)來處理編碼。
4 上傳文件同名問題(文件重命名)
通常我們會把用戶上傳的文件保存到uploads目錄下,但如果用戶上傳了同名文件呢?這會出現覆蓋的現象。處理這一問題的手段是使用UUID生成唯一名稱,然后再使用“_”連接文件上傳的原始名稱。
例如用戶上傳的文件是“我的一寸照片.jpg”,在通過處理后,文件名稱為:“891b3881395f4175b969256a3f7b6e10_我的一寸照片.jpg”,這種手段不會使文件丟失擴展名,並且因為UUID的唯一性,上傳的文件同名,但在服務器端是不會出現同名問題的。
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("utf-8"); DiskFileItemFactory dfif = new DiskFileItemFactory(); ServletFileUpload fileUpload = new ServletFileUpload(dfif); try { List<FileItem> list = fileUpload.parseRequest(request); //獲取第二個表單項,因為第一個表單項是username,第二個才是file表單項 FileItem fileItem = list.get(1); String name = fileItem.getName();//獲取文件名稱 // 如果客戶端使用的是IE6,那么需要從完整路徑中獲取文件名稱 int lastIndex = name.lastIndexOf("\\"); if(lastIndex != -1) { name = name.substring(lastIndex + 1); } // 獲取上傳文件的保存目錄 String savepath = this.getServletContext().getRealPath("/WEB-INF/uploads"); String uuid = CommonUtils.uuid();//生成uuid String filename = uuid + "_" + name;//新的文件名稱為uuid + 下划線 + 原始名稱 //創建file對象,下面會把上傳文件保存到這個file指定的路徑 //savepath,即上傳文件的保存目錄 //filename,文件名稱 File file = new File(savepath, filename); // 保存文件 fileItem.write(file); } catch (Exception e) { throw new ServletException(e); } } |
5 一個目錄不能存放過多的文件(存放目錄打散)
一個目錄下不應該存放過多的文件,一般一個目錄存放1000個文件就是上限了,如果在多,那么打開目錄時就會很“卡”。你可以嘗試打印C:\WINDOWS\system32目錄,你會感覺到的。
也就是說,我們需要把上傳的文件放到不同的目錄中。但是也不能為每個上傳的文件一個目錄,這種方式會導致目錄過多。所以我們應該采用某種算法來“打散”!
打散的方法有很多,例如使用日期來打散,每天生成一個目錄。也可以使用文件名的首字母來生成目錄,相同首字母的文件放到同一目錄下。
日期打散算法:如果某一天上傳的文件過多,那么也會出現一個目錄文件過多的情況;
首字母打散算法:如果文件名是中文的,因為中文過多,所以會導致目錄過多的現象。
我們這里使用hash算法來打散:
1. 獲取文件名稱的hashCode:int hCode = name.hashCode();;
2. 獲取hCode的低4位,然后轉換成16進制字符;
3. 獲取hCode的5~8位,然后轉換成16進制字符;
4. 使用這兩個16進制的字符生成目錄鏈。例如低4位字符為“5”
這種算法的好處是,在uploads目錄下最多生成16個目錄,而每個目錄下最多再生成16個目錄,即256個目錄,所有上傳的文件都放到這256個目錄下。如果每個目錄上限為1000個文件,那么一共可以保存256000個文件。
例如上傳文件名稱為:新建 文本文檔.txt,那么把“新建 文本文檔.txt”的哈希碼獲取到,再獲取哈希碼的低4位,和5~8位。假如低4位為:9,5~8位為1,那么文件的保存路徑為uploads/9/1/。
int hCode = name.hashCode();//獲取文件名的hashCode //獲取hCode的低4位,並轉換成16進制字符串 String dir1 = Integer.toHexString(hCode & 0xF); //獲取hCode的低5~8位,並轉換成16進制字符串 String dir2 = Integer.toHexString(hCode >>> 4 & 0xF); //與文件保存目錄連接成完整路徑 savepath = savepath + "/" + dir1 + "/" + dir2; //因為這個路徑可能不存在,所以創建成File對象,再創建目錄鏈,確保目錄在保存文件之前已經存在 new File(savepath).mkdirs(); |
6 上傳的單個文件的大小限制
限制上傳文件的大小很簡單,ServletFileUpload類的setFileSizeMax(long)就可以了。參數就是上傳文件的上限字節數,例如servletFileUpload.setFileSizeMax(1024*10)表示上限為10KB。
一旦上傳的文件超出了上限,那么就會拋出FileUploadBase.FileSizeLimitExceededException異常。我們可以在Servlet中獲取這個異常,然后向頁面輸出“上傳的文件超出限制”。
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("utf-8"); DiskFileItemFactory dfif = new DiskFileItemFactory(); ServletFileUpload fileUpload = new ServletFileUpload(dfif); // 設置上傳的單個文件的上限為10KB fileUpload.setFileSizeMax(1024 * 10); try { List<FileItem> list = fileUpload.parseRequest(request); //獲取第二個表單項,因為第一個表單項是username,第二個才是file表單項 FileItem fileItem = list.get(1); String name = fileItem.getName();//獲取文件名稱 // 如果客戶端使用的是IE6,那么需要從完整路徑中獲取文件名稱 int lastIndex = name.lastIndexOf("\\"); if(lastIndex != -1) { name = name.substring(lastIndex + 1); } // 獲取上傳文件的保存目錄 String savepath = this.getServletContext().getRealPath("/WEB-INF/uploads"); String uuid = CommonUtils.uuid();//生成uuid String filename = uuid + "_" + name;//新的文件名稱為uuid + 下划線 + 原始名稱 int hCode = name.hashCode();//獲取文件名的hashCode //獲取hCode的低4位,並轉換成16進制字符串 String dir1 = Integer.toHexString(hCode & 0xF); //獲取hCode的低5~8位,並轉換成16進制字符串 String dir2 = Integer.toHexString(hCode >>> 4 & 0xF); //與文件保存目錄連接成完整路徑 savepath = savepath + "/" + dir1 + "/" + dir2; //因為這個路徑可能不存在,所以創建成File對象,再創建目錄鏈,確保目錄在保存文件之前已經存在 new File(savepath).mkdirs(); //創建file對象,下面會把上傳文件保存到這個file指定的路徑 //savepath,即上傳文件的保存目錄 //filename,文件名稱 File file = new File(savepath, filename); // 保存文件 fileItem.write(file); } catch (Exception e) { // 判斷拋出的異常的類型是否為FileUploadBase.FileSizeLimitExceededException // 如果是,說明上傳文件時超出了限制。 if(e instanceof FileUploadBase.FileSizeLimitExceededException) { // 在request中保存錯誤信息 request.setAttribute("msg", "上傳失敗!上傳的文件超出了10KB!"); // 轉發到index.jsp頁面中!在index.jsp頁面中需要使用${msg}來顯示錯誤信息 request.getRequestDispatcher("/index.jsp").forward(request, response); return; } throw new ServletException(e); } } |
7 上傳文件的總大小限制
上傳文件的表單中可能允許上傳多個文件,例如:
有時我們需要限制一個請求的大小。也就是說這個請求的最大字節數(所有表單項之和)!實現這一功能也很簡單,只需要調用ServletFileUpload類的setSizeMax(long)方法即可。
例如fileUpload.setSizeMax(1024 * 10);,顯示整個請求的上限為10KB。當請求大小超出10KB時,ServletFileUpload類的parseRequest()方法會拋出FileUploadBase.SizeLimitExceededException異常。
8 緩存大小與臨時目錄
大家想一想,如果我上傳一個藍光電影,先把電影保存到內存中,然后再通過內存copy到服務器硬盤上,那么你的內存能吃的消么?
所以fileupload組件不可能把文件都保存在內存中,fileupload會判斷文件大小是否超出10KB,如果是那么就把文件保存到硬盤上,如果沒有超出,那么就保存在內存中。
10KB是fileupload默認的值,我們可以來設置它。
當文件保存到硬盤時,fileupload是把文件保存到系統臨時目錄,當然你也可以去設置臨時目錄。
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("utf-8"); DiskFileItemFactory dfif = new DiskFileItemFactory(1024*20, new File("F:\\temp")); ServletFileUpload fileUpload = new ServletFileUpload(dfif); try { List<FileItem> list = fileUpload.parseRequest(request); FileItem fileItem = list.get(1); String name = fileItem.getName(); String savepath = this.getServletContext().getRealPath("/WEB-INF/uploads"); // 保存文件 fileItem.write(path(savepath, name)); } catch (Exception e) { throw new ServletException(e); } } private File path(String savepath, String filename) { // 從完整路徑中獲取文件名稱 int lastIndex = filename.lastIndexOf("\\"); if(lastIndex != -1) { filename = filename.substring(lastIndex + 1); } // 通過文件名稱生成一級、二級目錄 int hCode = filename.hashCode(); String dir1 = Integer.toHexString(hCode & 0xF); String dir2 = Integer.toHexString(hCode >>> 4 & 0xF); savepath = savepath + "/" + dir1 + "/" + dir2; // 創建目錄 new File(savepath).mkdirs(); // 給文件名稱添加uuid前綴 String uuid = CommonUtils.uuid(); filename = uuid + "_" + filename; // 創建文件完成路徑 return new File(savepath, filename); } |
文件下載
2 通過Servlet下載1
被下載的資源必須放到WEB-INF目錄下(只要用戶不能通過瀏覽器直接訪問就OK),然后通過Servlet完成下載。
在jsp頁面中給出超鏈接,鏈接到DownloadServlet,並提供要下載的文件名稱。然后DownloadServlet獲取文件的真實路徑,然后把文件寫入到response.getOutputStream()流中。
download.jsp
<body> This is my JSP page. <br> <a href="<c:url value='/DownloadServlet?path=a.avi'/>">a.avi</a><br/> <a href="<c:url value='/DownloadServlet?path=a.jpg'/>">a.jpg</a><br/> <a href="<c:url value='/DownloadServlet?path=a.txt'/>">a.txt</a><br/> </body> |
DownloadServlet.java
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String filename = request.getParameter("path"); String filepath = this.getServletContext().getRealPath("/WEB-INF/uploads/" + filename); File file = new File(filepath); if(!file.exists()) { response.getWriter().print("您要下載的文件不存在!"); return; } IOUtils.copy(new FileInputStream(file), response.getOutputStream()); } |
上面代碼有如下問題:
l 可以下載a.avi,但在下載框中的文件名稱是DownloadServlet;
l 不能下載a.jpg和a.txt,而是在頁面中顯示它們。
3 通過Servlet下載2
下面來處理上一例中的問題,讓下載框中可以顯示正確的文件名稱,以及可以下載a.jpg和a.txt文件。
通過添加content-disposition頭來處理上面問題。當設置了content-disposition頭后,瀏覽器就會彈出下載框。
而且還可以通過content-disposition頭來指定下載文件的名稱!
String filename = request.getParameter("path"); String filepath = this.getServletContext().getRealPath("/WEB-INF/uploads/" + filename); File file = new File(filepath); if(!file.exists()) { response.getWriter().print("您要下載的文件不存在!"); return; } response.addHeader("content-disposition", "attachment;filename=" + filename); IOUtils.copy(new FileInputStream(file), response.getOutputStream()); |
雖然上面的代碼已經可以處理txt和jpg等文件的下載問題,並且也處理了在下載框中顯示文件名稱的問題,但是如果下載的文件名稱是中文的,那么還是不行的。
3 通過Servlet下載3
下面是處理在下載框中顯示中文的問題!
其實這一問題很簡單,只需要通過URL來編碼中文即可!
download.jsp
<a href="<c:url value='/DownloadServlet?path=這個殺手不太冷.avi'/>">這個殺手不太冷.avi</a><br/> <a href="<c:url value='/DownloadServlet?path=白冰.jpg'/>">白冰.jpg</a><br/> <a href="<c:url value='/DownloadServlet?path=說明文檔.txt'/>">說明文檔.txt</a><br/> |
DownloadServlet.java
String filename = request.getParameter("path"); // GET請求中,參數中包含中文需要自己動手來轉換。 // 當然如果你使用了“全局編碼過濾器”,那么這里就不用處理了 filename = new String(filename.getBytes("ISO-8859-1"), "UTF-8"); String filepath = this.getServletContext().getRealPath("/WEB-INF/uploads/" + filename); File file = new File(filepath); if(!file.exists()) { response.getWriter().print("您要下載的文件不存在!"); return; } // 所有瀏覽器都會使用本地編碼,即中文操作系統使用GBK // 瀏覽器收到這個文件名后,會使用iso-8859-1來解碼 filename = new String(filename.getBytes("GBK"), "ISO-8859-1"); response.addHeader("content-disposition", "attachment;filename=" + filename); IOUtils.copy(new FileInputStream(file), response.getOutputStream()); |
JavaMail
今日內容
l 郵件協議
l telnet訪問郵件服務器
l JavaMail
郵件協議
1 收發郵件
發郵件大家都會吧!發郵件是從客戶端把郵件發送到郵件服務器,收郵件是把郵件服務器的郵件下載到客戶端。
我們在163、126、QQ、sohu、sina等網站注冊的Email賬戶,其實就是在郵件服務器中注冊的。這些網站都有自己的郵件服務器。
2 郵件協議概述
與HTTP協議相同,收發郵件也是需要有傳輸協議的。
l SMTP:(Simple Mail Transfer Protocol,簡單郵件傳輸協議)發郵件協議;
l POP3:(Post Office Protocol Version 3,郵局協議第3版)收郵件協議;
l IMAP:(Internet Message Access Protocol,因特網消息訪問協議)收發郵件協議,我們的課程不涉及該協議。
3 理解郵件收發過程
其實你可以把郵件服務器理解為郵局!如果你需要給朋友寄一封信,那么你需要把信放到郵筒中,這樣你的信會“自動”到達郵局,郵局會把信郵到另一個省市的郵局中。然后這封信會被送到收信人的郵箱中。最終收信人需要自己經常查看郵箱是否有新的信件。
其實每個郵件服務器都由SMTP服務器和POP3服務器構成,其中SMTP服務器負責發郵件的請求,而POP3負責收郵件的請求。
當然,有時我們也會使用163的賬號,向126的賬號發送郵件。這時郵件是發送到126的郵件服務器,而對於163的郵件服務器是不會存儲這封郵件的。
4 郵件服務器名稱
smtp服務器的端口號為25,服務器名稱為smtp.xxx.xxx。
pop3服務器的端口號為110,服務器名稱為pop3.xxx.xxx。
例如:
l 163:smtp.163.com和pop3.163.com;
l 126:smtp.126.com和pop3.126.com;
l qq:smtp.qq.com和pop3.qq.com;
l sohu:smtp.sohu.com和pop3.sohu.com;
l sina:smtp.sina.com和pop3.sina.com。
telnet收發郵件
1 BASE64加密
BASE64是一種加密算法,這種加密方式是可逆的!它的作用是使加密后的文本無法用肉眼識別。Java提供了sun.misc.BASE64Encoder這個類,用來對做Base64的加密和解密,但我們知道,使用sun包下的東西會有警告!甚至在eclipse中根本使用不了這個類(需要設置),所以我們還是聽sun公司的話,不要去使用它內部使用的類,我們去使用apache commons組件中的codec包下的Base64這個類來完成BASE64加密和解密。
package cn.itcast; import org.apache.commons.codec.binary.Base64; public class Base64Utils { public static String encode(String s) { return encode(s, "utf-8"); } public static String decode(String s) { return decode(s, "utf-8"); } public static String encode(String s, String charset) { try { byte[] bytes = s.getBytes(charset); bytes = Base64.encodeBase64(bytes); return new String(bytes, charset); } catch (Exception e) { throw new RuntimeException(e); } } public static String decode(String s, String charset) { try { byte[] bytes = s.getBytes(charset); bytes = Base64.decodeBase64(bytes); return new String(bytes, charset); } catch (Exception e) { throw new RuntimeException(e); } } } |
2 telnet發郵件
連接163的smtp服務器:;
連接成功后需要如下步驟才能發送郵件:
1 與服務器打招呼:ehlo你的名字
2 發出登錄請求:auth login
3 輸入加密后的郵箱名:(itcast_cxf@163.com)aXRjYXN0X2N4ZkAxNjMuY29t
4 輸入加密后的郵箱密碼:(itcast)aXRjYXN0
5 輸入誰來發送郵件,即from:mail from:<itcast_cxf@163.com>
6 輸入把郵件發給誰,即to:rcpt to:<itcast_cxf@126.com>
7 發送填寫數據請求:data
8 開始輸入數據,數據包含:from、to、subject,以及郵件內容,如果輸入結束后,以一個“.”為一行,表示輸入結束:
from:<zhangBoZhi@163.com>
to:<itcast_cxf@sina.com>
subject: 我愛上你了
我已經深深的愛上你了,我是張柏芝。
.
注意,在標題和郵件正文之間要有一個空行!當要退出時,一定要以一個“.”為單行,表示輸入結束。
9 最后一步:quit
telnet收郵件
1 telnet收郵件的步驟
pop3無需使用Base64加密!!!
收郵件連接的服務器是pop3.xxx.com,pop3協議的默認端口號是110。請注意!這與發郵件完全不同。如果你在163有郵箱賬戶,那么你想使用telnet收郵件,需要連接的服務器是pop3.163.com。
l 連接pop3服務器:telnet pop3.163.com 110
l user命令:user 用戶名,例如:user itcast_cxf@163.com;
l pass命令:pass 密碼,例如:pass itcast;
l stat命令:stat命令用來查看郵箱中郵件的個數,所有郵件所占的空間;
l list命令:list命令用來查看所有郵件,或指定郵件的狀態,例如:list 1是查看第一封郵件的大小,list是查看郵件列表,即列出所有郵件的編號,及大小;
l retr命令:查看指定郵件的內容,例如:retr 1#是查看第一封郵件的內容;
l dele命令:標記某郵件為刪除,但不是馬上刪除,而是在退出時才會真正刪除;
l quit命令:退出!如果在退出之前已經使用dele命令標記了某些郵件,那么會在退出是刪除它們。
JavaMail
1 JavaMail概述
Java Mail是由SUN公司提供的專門針對郵件的API,主要Jar包:mail.jar、activation.jar。
在使用MyEclipse創建web項目時,需要小心!如果只是在web項目中使用java mail是沒有什么問題的,發布到Tomcat上運行一點問題都沒有!
但是如果是在web項目中寫測試那就出問題了。
在MyEclipse中,會自動給web項目導入javax.mail包中的類,但是不全(其實是只有接口,而沒有接口的實現類),所以只靠MyEclipse中的類是不能運行java mail項目的,但是如果這時你再去自行導入mail.jar時,就會出現沖突。
處理方案:到下面路徑中找到javaee.jar文件,把javax.mail刪除!!!
D:\Program Files\MyEclipse\Common\plugins\com.genuitec.eclipse.j2eedt.core_10.0.0.me201110301321\data\libraryset\EE_5
2 JavaMail中主要類
java mail中主要類:javax.mail.Session、javax.mail.internet.MimeMessage、javax.mail.Transport。
Session:表示會話,即客戶端與郵件服務器之間的會話!想獲得會話需要給出賬戶和密碼,當然還要給出服務器名稱。在郵件服務中的Session對象,就相當於連接數據庫時的Connection對象。
MimeMessage:表示郵件類,它是Message的子類。它包含郵件的主題(標題)、內容,收件人地址、發件人地址,還可以設置抄送和暗送,甚至還可以設置附件。
Transport:用來發送郵件。它是發送器!
3 JavaMail之Hello World
在使用telnet發郵件時,還需要自己來處理Base64編碼的問題,但使用JavaMail就不必理會這些問題了,都由JavaMail來處理。
第一步:獲得Session
Session session = Session.getInstance(Properties prop, Authenticator auth);
其中prop需要指定兩個鍵值,一個是指定服務器主機名,另一個是指定是否需要認證!我們當然需要認證!
Properties prop = new Properties();
prop.setProperty(“mail.host”, “smtp.163.com”);//設置服務器主機名
prop.setProperty(“mail.smtp.auth”, “true”);//設置需要認證
其中Authenticator是一個接口表示認證器,即校驗客戶端的身份。我們需要自己來實現這個接口,實現這個接口需要使用賬戶和密碼。
Authenticator auth = new Authenticator() {
public PasswordAuthentication getPasswordAuthentication () {
new PasswordAuthentication(“itcast_cxf”, “itcast”);//用戶名和密碼
}
};
通過上面的准備,現在可以獲取得Session對象了:
Session session = Session.getInstance(prop, auth);
第二步:創建MimeMessage對象
創建MimeMessage需要使用Session對象來創建:
MimeMessage msg = new MimeMessage(session);
然后需要設置發信人地址、收信人地址、主題,以及郵件正文。
msg.setFrom(new InternetAddress(“itcast_cxf@163.com”));//設置發信人
msg.addRecipients(RecipientType.TO, “itcast_cxf@qq.com,itcast_cxf@sina.com”);//設置多個收信人
msg.addRecipients(RecipientType.CC, “itcast_cxf@sohu.com,itcast_cxf@126.com”);//設置多個抄送
msg.addRecipients(RecipientType.BCC, ”itcast_cxf@hotmail.com”);//設置暗送
msg.setSubject(“這是一封測試郵件”);//設置主題(標題)
msg.setContent(“當然是hello world!”, “text/plain;charset=utf-8”);//設置正文
第三步:發送郵件
Transport.send(msg);//發送郵件
4 JavaMail發送帶有附件的郵件(了解)
一封郵件可以包含正文、附件N個,所以正文與N個附件都是郵件的一個部份。
上面的hello world案例中,只是發送了帶有正文的郵件!所以在調用setContent()方法時直接設置了正文,如果想發送帶有附件郵件,那么需要設置郵件的內容為MimeMultiPart。
MimeMulitpart parts = new MimeMulitpart();//多部件對象,可以理解為是部件的集合
msg.setContent(parts);//設置郵件的內容為多部件內容。
然后我們需要把正文、N個附件創建為“主體部件”對象(MimeBodyPart),添加到MimeMuiltPart中即可。
MimeBodyPart part1 = new MimeBodyPart();//創建一個部件
part1.setCotnent(“這是正文部分”, “text/html;charset=utf-8”);//給部件設置內容
parts.addBodyPart(part1);//把部件添加到部件集中。
下面我們創建一個附件:
MimeBodyPart part2 = new MimeBodyPart();//創建一個部件
part2.attachFile(“F:\\a.jpg”);//設置附件
part2.setFileName(“hello.jpg”);//設置附件名稱
parts.addBodyPart(part2);//把附件添加到部件集中
注意,如果在設置文件名稱時,文件名稱中包含了中文的話,那么需要使用MimeUitlity類來給中文編碼:
part2.setFileName(MimeUitlity.encodeText(“美女.jpg”));