Spring實(shí)戰(zhàn)6-利用Spring和JDBC訪問數(shù)據(jù)庫

主要內(nèi)容

  • 定義Spring的數(shù)據(jù)訪問支持
  • 配置數(shù)據(jù)庫資源
  • 使用Spring提供的JDBC模板

寫在前面:經(jīng)過上一篇文章的學(xué)習(xí),我們掌握了如何寫web應(yīng)用的控制器層浦箱,不過由于只定義了SpitterRepositorySpittleRepository接口,在本地啟動(dòng)該web服務(wù)的時(shí)候會(huì)遇到控制器無法注入對(duì)應(yīng)的bean的錯(cuò)誤构灸,因此我決定跳過6~9章泽裳,先搞定數(shù)據(jù)庫訪問者一章。

在企業(yè)級(jí)應(yīng)用開發(fā)中不可避免得會(huì)涉及到數(shù)據(jù)持久化層淋纲,在數(shù)據(jù)持久化層的開發(fā)過程中,可能遇到很多陷阱触机。你需要初始化數(shù)據(jù)庫訪問框架帚戳、打開數(shù)據(jù)庫連接、處理各種異常儡首,最后還要記得關(guān)閉連接片任。如果在這些步驟中你有一步做錯(cuò)了,那就又丟失公司數(shù)據(jù)的風(fēng)險(xiǎn)蔬胯。妥當(dāng)?shù)锰幚磉@些并不容易对供,Spring提供了一套完整的數(shù)據(jù)庫訪問框架,用于簡化各種數(shù)據(jù)庫訪問技術(shù)的使用氛濒。

在開發(fā)Spttr應(yīng)用的持久層時(shí)产场,你需要在JDBC、Hibernate舞竿、Java Perssitence或者其他ORM框架等技術(shù)中進(jìn)行選擇京景。Spring扮演的角色是盡量消除你在使用這些技術(shù)時(shí)需要寫的重復(fù)代碼,以便開發(fā)人員專注于業(yè)務(wù)邏輯骗奖。

10.1 學(xué)習(xí)Spring的數(shù)據(jù)庫訪問哲學(xué)

Spring框架的目標(biāo)之一就是讓開發(fā)者面向接口編程确徙,Spring的數(shù)據(jù)訪問支持也不例外醒串。

和很多其他應(yīng)用一樣,Spittr應(yīng)用也需要從數(shù)據(jù)庫中讀取信息或者寫入信息到數(shù)據(jù)庫鄙皇。為了避免持久化相關(guān)的代碼遍布應(yīng)用的各個(gè)地方芜赌,一般我們會(huì)將這些任務(wù)整合到一個(gè)模塊中完成,這類模塊通常被稱之為數(shù)據(jù)訪問對(duì)象(DAOs)或者repositories伴逸。

為了避免業(yè)務(wù)層模塊強(qiáng)依賴于某種類型的數(shù)據(jù)庫(關(guān)系型orNoSQL)缠沈,數(shù)據(jù)庫訪問層應(yīng)以接口形式對(duì)外提供服務(wù)。下圖展示了這個(gè)思路:

service層不自己處理數(shù)據(jù)訪問错蝴,將這個(gè)任務(wù)委托給repositories洲愤;repository的接口使得service對(duì)象與具體的數(shù)據(jù)庫訪問策略松耦合

如你所見,service對(duì)象通過接口訪問repository對(duì)象漱竖,這有很多好處:(1)因?yàn)閟ervice對(duì)象并不限制于某個(gè)特定的數(shù)據(jù)訪問實(shí)現(xiàn)禽篱,這使得service對(duì)象便于測(cè)試畜伐;(2)你可以創(chuàng)建這些數(shù)據(jù)庫訪問接口的mock實(shí)現(xiàn)馍惹,這樣即使沒有建立數(shù)據(jù)庫連接你也可以測(cè)試service對(duì)象;(3)可以顯著加速單元測(cè)試的執(zhí)行速度玛界;(4)可以避免某個(gè)測(cè)試用例因數(shù)據(jù)不一致而失敗万矾。

數(shù)據(jù)訪問層通過repository接口中的幾個(gè)方法與service層溝通,這使得應(yīng)用設(shè)計(jì)非常靈活慎框,即使將來要更換數(shù)據(jù)庫持久層框架良狈,對(duì)應(yīng)用的其他部分的影響也非常小。如果數(shù)據(jù)訪問層的實(shí)現(xiàn)細(xì)節(jié)散步到應(yīng)用的其他部分笨枯,則整個(gè)應(yīng)用跟數(shù)據(jù)訪問層緊密耦合在一起薪丁。

INTERFACES AND SPRING 如果你讀完上面兩段話之后能夠感覺到我有很強(qiáng)的意愿將持久化層隱藏在接口之后,那說明我正確得表達(dá)了自己的想法馅精。我相信接口是書寫松耦合的代碼的關(guān)鍵严嗜,不僅是數(shù)據(jù)庫訪問層,應(yīng)該在應(yīng)用的所有模塊之間使用接口進(jìn)行交互洲敢。雖然Spring并沒有強(qiáng)制要求面向接口編程漫玄,但是Spring的設(shè)計(jì)思想鼓勵(lì)面向接口編程——最好通過接口將一個(gè)bean裝配到另一個(gè)bean的屬性中。

Spring提供了方便的異常體系压彭,也可以幫助開發(fā)者隔離數(shù)據(jù)庫訪問層與應(yīng)用的其他模塊睦优。

10.1.1 了解Spring的數(shù)據(jù)訪問的異常體系

在使用原始的JDBC接口時(shí),如果你不捕獲SQLException壮不,就不能做任何事情汗盘。SQLException的意思是在嘗試訪問數(shù)據(jù)庫過程中發(fā)生了某些錯(cuò)誤,但是并沒有提供足夠的信息告訴開發(fā)人員具體的錯(cuò)誤原因以及如何修正錯(cuò)誤询一。

下列這些情況都可能引發(fā)SQLException

  • 連接數(shù)據(jù)庫失斠酢尸执;
  • 查詢語句中存在語法錯(cuò)誤;
  • 查詢中提到的表或者列不存在缓醋;
  • 插入或者更新操作違背了數(shù)據(jù)庫一致性如失;

關(guān)于SQLException最大的問題在于:當(dāng)捕獲它的時(shí)候應(yīng)該如何處理。調(diào)查顯示送粱,很多引起SQLException的故障不能在catch代碼塊中修復(fù)褪贵。大部分被拋出的SQLException表示應(yīng)用發(fā)生了致命故障。如果應(yīng)用不能連接數(shù)據(jù)庫抗俄,通常意味著應(yīng)用不能繼續(xù)執(zhí)行脆丁;同樣地,如果在查詢語句中有錯(cuò)誤动雹,在運(yùn)行時(shí)能做的工作也很少槽卫。

既然我們并不能做些操作來恢復(fù)SQLException,為什么必須捕獲它胰蝠?

即使你計(jì)劃處理一些SQLException歼培,你也必須捕獲SQLException對(duì)象然后查看它的屬性才能發(fā)掘出問題的本質(zhì)。這是因?yàn)?em>SQLException是一個(gè)代之所有數(shù)據(jù)庫訪問相關(guān)問題的異常茸塞,而不是針對(duì)每個(gè)可能的問題定義一個(gè)異常類型躲庄。

一些持久化框架提供了豐富的異常體系。例如钾虐,Hibernate提供了幾乎兩打不通的異常噪窘,每種代表一個(gè)特定的數(shù)據(jù)庫訪問問題。這令使用Hibernate的開發(fā)者可以為自己想處理的異常書寫catch塊效扫。

即使這樣倔监,Hibernate的異常也只對(duì)Hibernate框架有用,如果你使用Hibernate自己的異常體系菌仁,就可能使程序的剩余部分強(qiáng)依賴于Hibernate浩习,將來如果想升級(jí)為其他的持久化框架會(huì)非常麻煩。在這節(jié)開頭的時(shí)候說過掘托,我們希望隔離數(shù)據(jù)訪問層和持久化機(jī)制的特性瘦锹。如果在數(shù)據(jù)訪問層處理Hibernate框架拋出的專屬異常,則會(huì)影響到應(yīng)用中的其余模塊闪盔;如果不這么做弯院,你必須捕獲該持久化的專屬異常,然后重新拋出一個(gè)平臺(tái)無關(guān)的異常泪掀。

SPRING'S PERSISTENCE PLATFORM-AGNOSTIC EXCEPTION

一方面听绳,JDBC提供的異常體系過于普遍——根本沒有異常體系可言;另一方面异赫,Hibernate的異常體系是針對(duì)這個(gè)框架自己的椅挣,因此我們需要一套數(shù)據(jù)庫訪問的異常體系头岔,既具備足夠強(qiáng)的描述能力,又不要跟具體的持久化框架直接關(guān)聯(lián)鼠证。

Spring JDBC提供的異常體系同時(shí)滿足上述兩個(gè)條件峡竣。不同于傳統(tǒng)的JDBC,Spring JDBC針對(duì)某些具體的問題定義了對(duì)應(yīng)的數(shù)據(jù)庫訪問異常量九。下表展示了Spring 數(shù)據(jù)訪問異常和JDBC的異常之間的對(duì)應(yīng)關(guān)系适掰。

JDBC的異常 VS Spring 的數(shù)據(jù)庫訪問異常

如你所見,Spring為在讀取或者寫入數(shù)據(jù)庫時(shí)可能出錯(cuò)的原因設(shè)置了對(duì)應(yīng)的異常類型荠列,Spring 實(shí)際提供的數(shù)據(jù)庫訪問異常要遠(yuǎn)多于表10.1所列出的那些类浪。

Spring在提供如此豐富的異常前提下,還保證這些異常類型跟具體的持久化機(jī)制隔離肌似。這意味著無論你使用什么持久化框架费就,你都可以使用同一套異常定義——持久化機(jī)制的選擇與數(shù)據(jù)訪問層實(shí)現(xiàn)解耦合。

LOOK, MA! NO CATCH BLOCKS!

表10.1中沒有說明的是:所有這些異常的根對(duì)象是DataAccessException川队,這是一個(gè)unchecked exception力细。換句話說,Spring不會(huì)強(qiáng)制你捕獲這些數(shù)據(jù)庫訪問異常呼寸。

Spring通過提供unchecked exception艳汽,讓開發(fā)者決定是否需要捕獲并處理某個(gè)異常猴贰。為了充分發(fā)揮Spring的數(shù)據(jù)訪問異常对雪,你最好使用Spring提供的數(shù)據(jù)訪問模板。

10.1.2 模式化數(shù)據(jù)訪問

如果你之前通過飛機(jī)出行過米绕,你一定明白在行程過程中最重要的事情是將行李從A地托運(yùn)到B地瑟捣。要妥當(dāng)?shù)猛瓿蛇@個(gè)事情需要很多步驟:當(dāng)你到達(dá)機(jī)場(chǎng)時(shí),你首先需要檢查行李栅干;然后需要通過機(jī)場(chǎng)的安全掃描迈套,以免不小心將可能危害飛行安全的東西帶上飛機(jī);然后行李需要通過長長的傳送帶被運(yùn)上飛機(jī)碱鳞。如果你需要轉(zhuǎn)乘航線桑李,行李也需要跟著你一起運(yùn)輸。當(dāng)你到達(dá)最終目的地時(shí)窿给,行李會(huì)被運(yùn)下飛機(jī)然后放置在傳送帶上贵白,最后,你需要去目的地機(jī)場(chǎng)的指定地點(diǎn)領(lǐng)取自己的行李崩泡。

雖然在這個(gè)過程中有需要步驟禁荒,但是你僅僅需要參與其中的一部分。在這個(gè)例子中角撞,整個(gè)過程就是將行李從出發(fā)城市運(yùn)輸?shù)侥康某鞘星喊椋@個(gè)過程是固定的不會(huì)改變勃痴。在運(yùn)輸過程可以分成明確的幾步:檢查行李、裝載行李热康、卸載行李等沛申。在這其中一些步驟也是固定的,每次都一樣:當(dāng)飛機(jī)到達(dá)目的地之后姐军,所有行李都需要卸載并放在機(jī)場(chǎng)的指定地點(diǎn)污它。

在指定的節(jié)點(diǎn),總程序會(huì)將一部分工作委托給一個(gè)子程序庶弃,用于完成更加細(xì)節(jié)的任務(wù)衫贬,這就是總程序中的變量部分。例如歇攻,行李的托運(yùn)開始于乘客自己檢查行李固惯,因?yàn)槊總€(gè)乘客的動(dòng)作都不相同——各自檢查自己的行李,因此總程序中的這個(gè)步驟如何執(zhí)行具體取決于每個(gè)乘客缴守。用軟件開發(fā)中的術(shù)語描述葬毫,上述過程就是模板模式:模板方法規(guī)定整個(gè)算法的執(zhí)行過程,將每個(gè)步驟的具體細(xì)節(jié)通過接口委托給子類完成屡穗。

Spring提供的數(shù)據(jù)訪問支持也使用了模板模式贴捡。無論你選擇使用什么技術(shù),數(shù)據(jù)訪問的步驟就是固定的幾步(例如村砂,在開始時(shí)烂斋,你一定需要獲取一個(gè)數(shù)據(jù)庫連接;在操作完成后础废,你一定需要釋放之前獲取的資源)汛骂,但是每一步具體怎么實(shí)現(xiàn)有所不同。你用不同的方法查詢或者更新不同的數(shù)據(jù)评腺,這些屬于數(shù)據(jù)庫訪問過程中的變量帘瞭。

Spring將數(shù)據(jù)訪問過程中的固定步驟和變量部分分為兩類:模板(templates)和回調(diào)函數(shù)(callbacks)。模板負(fù)責(zé)管理數(shù)據(jù)訪問過程中的固定步驟蒿讥,而由你定制的業(yè)務(wù)邏輯則寫在回調(diào)函數(shù)中蝶念。下圖顯示了這兩類對(duì)象的責(zé)任和角色:

Spring的數(shù)據(jù)訪問模板類負(fù)責(zé)數(shù)據(jù)訪問過程中的通用操作;與業(yè)務(wù)邏輯相關(guān)的任務(wù)則通過回調(diào)函數(shù)由開發(fā)者定制

如你所見芋绸,Spring的模板類處理數(shù)據(jù)訪問的固定步驟——事務(wù)管理媒殉、資源管理和異常處理;與此同時(shí)侥钳,跟應(yīng)用相關(guān)的數(shù)據(jù)訪問任務(wù)——?jiǎng)?chuàng)建語句适袜、綁定參數(shù)和處理結(jié)果集等,則需要在回調(diào)函數(shù)中完成舷夺。這種框架十分優(yōu)雅苦酱,作為開發(fā)人員你只需要關(guān)注具體的數(shù)據(jù)訪問邏輯售貌。

Spring提供了集中不同的模板,開發(fā)者根據(jù)項(xiàng)目中使用的持久化框架選擇對(duì)應(yīng)的模板工具類疫萤。如果你使用原始的JDBC方式颂跨,則可以使用JdbcTemplate;如果你更傾向于使用ORM框架扯饶,則可以使用HibernateTemplateJpaTemplate恒削。表10.2列出了Spring提供的數(shù)據(jù)訪問模板。

Spring為不同持久化技術(shù)提供了對(duì)應(yīng)的數(shù)據(jù)訪問模板

Spring為不同的持久化技術(shù)提供了對(duì)應(yīng)的數(shù)據(jù)訪問模板尾序,在這一章中并不能一一講述钓丰。因此我們將選擇最有效和你最可能使用的進(jìn)行講解。

這一章首先介紹JDBC技術(shù)每币,因?yàn)樗詈唵涡。辉诤竺孢€會(huì)介紹Hibernate和JPA——兩種最流行的基于POJO的ORM框架。PS:除了《Spring in Action》中的這幾種持久化技術(shù)兰怠,現(xiàn)在更加流行的是Mybatis框架梦鉴,后續(xù)我會(huì)專門寫對(duì)應(yīng)的總結(jié)和學(xué)習(xí)筆記。

但是揭保,所有這些持久化框架都需要依賴于具體的數(shù)據(jù)源肥橙,因此在開始學(xué)習(xí)templates和repositories之前,需要學(xué)習(xí)在Spring中如何配置數(shù)據(jù)源——用于連接數(shù)據(jù)庫秸侣。

10.2 配置數(shù)據(jù)源

Spring提供了幾種配置數(shù)據(jù)源的方式存筏,列舉如下:

  • 通過JDBC驅(qū)動(dòng)定義數(shù)據(jù)源;
  • 從JNDI中查詢數(shù)據(jù)源塔次;
  • 從連接池中獲取數(shù)據(jù)源方篮;

對(duì)于生產(chǎn)級(jí)別的應(yīng)用,我建議使用從數(shù)據(jù)庫連接池中獲取的數(shù)據(jù)源励负;如果有可能,也可以通過JNDI從應(yīng)用服務(wù)器中獲取數(shù)據(jù)源匕得;接下來首先看下如何配置Spring應(yīng)用從JNDI獲取數(shù)據(jù)源继榆。

10.2.1 使用JNDI數(shù)據(jù)源

Spring應(yīng)用一般部署在某個(gè)J2EE容器中,例如WebSphere汁掠、JBoss或者Tomcat略吨。開發(fā)者可以在這些服務(wù)器中配置數(shù)據(jù)源,一遍Spring應(yīng)用通過JNDI獲取考阱。按照這種方式配置數(shù)據(jù)源的好處在于:數(shù)據(jù)源配置在應(yīng)用外部翠忠,允許應(yīng)用在啟動(dòng)完成時(shí)再請(qǐng)求數(shù)據(jù)源進(jìn)行數(shù)據(jù)訪問;而且乞榨,數(shù)據(jù)源配置在應(yīng)用服務(wù)器中有助于提高性能秽之,且系統(tǒng)管理員可以進(jìn)行熱切換当娱。

首先,需要在tomcat中配置數(shù)據(jù)源考榨,方法參見stackoverflowHow to use JNDI DataSource provided by Tomcat in Spring?

在SpringXML配置文件中使用<jee:jndi-lookup>元素定義數(shù)據(jù)源對(duì)應(yīng)的Spring bean跨细。Spring應(yīng)用根據(jù)jndi-name從Tomcat容器中查找數(shù)據(jù)源;如果應(yīng)用是運(yùn)行Java應(yīng)用服務(wù)器中河质,則需要設(shè)置resource-ref為true冀惭,這樣在查詢的時(shí)候會(huì)在jndi-name指定的名字前面加上java:comp/env/

<jee:jndi-lookup id="dataSource"
        jndi-name="/jdbc/SpitterDS"
        resource-ref="true" />

如果你使用JavaConfig掀鹅,則可以使用JndiObjectFactoryBean從JNDI中獲取DataSource

@Bean
public JndiObjectFactoryBean dataSource() {
    JndiObjectFactoryBean jndiObjectFB = new JndiObjectFactoryBean();
    jndiObjectFB.setJndiName("/jdbc/SpittrDS");
    jndiObjectFB.setResourceRef(true);
    jndiObjectFB.setProxyInterface(javax.sql.DataSource.class);
    return jndiObjectFB;
}

顯然散休,在這里Java配置文件需要寫更多代碼,一般而言JavaConfig要比XML配置文件更簡單乐尊,這是個(gè)例外溃槐。

10.2.2 使用數(shù)據(jù)庫連接池

盡管Spring自身不提供數(shù)據(jù)連接池,但可以和很多第三方庫集成使用科吭,例如:

最常用的是DBCP昏滴,首先需要在pom文件中添加對(duì)應(yīng)的依賴,代碼如下:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-dbcp2</artifactId>
    <version>2.0</version>
</dependency>

關(guān)于commons-dbcp版本的區(qū)別:commons-dbcp現(xiàn)在分成了2個(gè)大版本对人,不同的版本要求的JDK不同:

  • DBCP 2.X compiles and runs under Java 7 only (JDBC 4.1)
  • DBCP 1.4 compiles and runs under Java 6 only (JDBC 4)

如果在XML文件中使用谣殊,則可以使用下列代碼配置DBCP的BasicDataSource

<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource"
      p:driverClassName="org.h2.Driver"
      p:url="jdbc:h2:tcp://localhost/~/spitter"
      p:username="sa"
      p:password=""
      p:initialSize="5" />

如果你使用Java配置文件,則可以使用下列代碼配置DataSourcebean牺弄。

@Bean
public BasicDataSource dataSource() {
    BasicDataSource ds = new BasicDataSource();
    ds.setDriverClassName("org.h2.Driver");
    ds.setUrl("jdbc:h2:tcp://localhost/~/spitter");
    ds.setUsername("sa");
    ds.setPassword("");
    ds.setInitialSize(5);
    return ds;
}

前四個(gè)屬性屬于配置BasicDataSource的必備屬性姻几,driverClassName指定JDBC驅(qū)動(dòng)類的全稱,這里我們配置了H2數(shù)據(jù)庫的驅(qū)動(dòng)势告;url屬性用于設(shè)置完整的數(shù)據(jù)庫地址蛇捌;usernamepassword分別指定用戶名和密碼。BasicDataSource中還有其他的屬性咱台,可以設(shè)置數(shù)據(jù)連接池的屬性络拌,例如,initialSize屬性用于指定連接池初始化時(shí)建立幾個(gè)數(shù)據(jù)庫連接回溺。對(duì)于dbcp1.4系列春贸,BasicDataSource的屬性可列舉如下表10.3所示:

dbcp1.4的*BasicDataSource*

對(duì)于dbcp2.x系列,如果你希望了解更多BasicDataSource的屬性遗遵,可參照官方文檔:dbcp2配置萍恕。

10.2.3 使用基于JDBC驅(qū)動(dòng)的數(shù)據(jù)源

在Spring中最簡單的數(shù)據(jù)源就是通過JDBC驅(qū)動(dòng)配置的數(shù)據(jù)源。Spring提供了三個(gè)相關(guān)的類供開發(fā)者選擇(都在org.springframework.jdbc.datasource包中):

  • DriverManagerDataSource——每次請(qǐng)求連接時(shí)都返回新的連接车要,用過的連接會(huì)馬上關(guān)閉并釋放資源允粤;
  • SimpleDriverDataSource——功能和DriverManagerDataSource相同,不同之處在于該類直接和JDBC驅(qū)動(dòng)交互,免去了類在特定環(huán)境(如OSGi容器)中可能遇到的類加載問題类垫。
  • SingleConnectionDataSource——每次都返回同一個(gè)連接對(duì)象司光,可以理解為只有1個(gè)連接的數(shù)據(jù)源連接池。

配置這些數(shù)據(jù)源跟之前配置DBCP的BasicDataSource類似阔挠,例如飘庄,可以用下列代碼配置DriverManagerDataSource

@Bean
public DataSource dataSource() {
    DriverManagerDataSource ds = new DriverManagerDataSource();
    ds.setDriverClassName("org.h2.Driver");
    ds.setUrl("jdbc:h2:tcp://localhost/~/spitter");
    ds.setUsername("sa");
    ds.setPassword("");
    return ds;
}

上述配置代碼的XML形式如下:

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"
      p:driverClassName="org.h2.Driver"
      p:url="jdbc:h2:tcp://localhost/~/spitter"
      p:username="sa"
      p:password="" />

由于上述這三個(gè)數(shù)據(jù)源對(duì)象對(duì)多線程應(yīng)用的支持都不好,因此強(qiáng)烈建議直接使用數(shù)據(jù)庫連接池购撼。

10.2.4 使用嵌入式數(shù)據(jù)源

嵌入式數(shù)據(jù)源作為應(yīng)用的一部分運(yùn)行跪削,非常適合在開發(fā)和測(cè)試環(huán)境中使用,但是不適合用于生產(chǎn)環(huán)境迂求。因?yàn)樵谑褂们度胧綌?shù)據(jù)源的情況下碾盐,你可以在每次應(yīng)用啟動(dòng)或者每次運(yùn)行單元測(cè)試之前初始化測(cè)試數(shù)據(jù)。

使用Spring的jdbc名字空間配置嵌入式數(shù)據(jù)源非常簡單揩局,下列代碼顯示了如何使用jdbc名字空間配置嵌入式的H2數(shù)據(jù)庫毫玖,并配置需要初始化的數(shù)據(jù)。

<jdbc:embedded-database id="dataSource" type="H2">
    <jdbc:script location="classpath*:schema.sql" />
    <jdbc:script location="classpath*:test-data.sql" />
</jdbc:embedded-database>

<jdbc:embedded-database>type屬性設(shè)置為H2表明嵌入式數(shù)據(jù)庫的類型是H2數(shù)據(jù)庫(確保引入了H2的依賴庫)凌盯。在<jdbc:embedded-database>配置中付枫,可以配置多個(gè)<jdbc:script>元素,用于設(shè)置和初始化數(shù)據(jù)庫:在這個(gè)例子中驰怎,schema.sql文件中包含用于創(chuàng)建數(shù)據(jù)表的關(guān)系阐滩;test-data.sql文件中用于插入測(cè)試數(shù)據(jù)。

如果你使用JavaConfig县忌,則可以使用EmbeddedDatabaseBuilder構(gòu)建嵌入式數(shù)據(jù)源:

@Bean
public DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .addScript("classpath*:schema.sql")
            .addScript("classpath*:test-data.sql")
            .build();
}

可以看出掂榔,setType()方法的作用等同于<jdbc:embedded-database>元素的type屬性,addScript()方法的作用等同于<jdbc:script>元素症杏。

10.2.5 使用profiles選擇數(shù)據(jù)源

一般需要在不同的環(huán)境(日常環(huán)境装获、性能測(cè)試環(huán)境、預(yù)發(fā)環(huán)境和生產(chǎn)環(huán)境等等)中配置不同的數(shù)據(jù)源厉颤,例如穴豫,在開發(fā)時(shí)非常適合使用嵌入式數(shù)據(jù)源、在QA環(huán)境中比較適合使用DBCP的BasicDataSource走芋、在生產(chǎn)環(huán)境中則適合使用<jee:jndi-lookup>元素绩郎,即使用JNDI查詢數(shù)據(jù)源。

Spring實(shí)戰(zhàn)3:裝配bean的進(jìn)階知識(shí)一文中我們探討過Spring的bean-profiles特性,這里就需要給不同的數(shù)據(jù)源配置不同的profiles,Java配置文件的內(nèi)容如下所示:

package org.test.spittr.config;

import org.apache.commons.dbcp2.BasicDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.jndi.JndiObjectFactoryBean;
import javax.sql.DataSource;

@Configuration
public class DataSourceConfiguration {
    @Profile("development")
    @Bean
    public DataSource embeddedDataSource() {
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .addScript("classpath*:schema.sql")
                .addScript("classpath*:test-data.sql")
                .build();
    }

    @Profile("qa")
    @Bean
    public BasicDataSource basicDataSource() {
        BasicDataSource ds = new BasicDataSource();
        ds.setDriverClassName("org.h2.Driver");
        ds.setUrl("jdbc:h2:tcp://localhost/~/spitter");
        ds.setUsername("sa");
        ds.setPassword("");
        ds.setInitialSize(5); //初始大小
        ds.setMaxTotal(10); //數(shù)據(jù)庫連接池大小
        return ds;
    }

    @Profile("production")
    @Bean
    public DataSource dataSource() {
        JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
        jndiObjectFactoryBean.setJndiName("/jdbc/SpittrDS");
        jndiObjectFactoryBean.setResourceRef(true);
        jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
        return (DataSource)jndiObjectFactoryBean.getObject();
    }
}

利用@Profile注解栏饮,Spring應(yīng)用可以運(yùn)行時(shí)再根據(jù)激活的profile選擇指定的數(shù)據(jù)源局服。在上述代碼中,當(dāng)development對(duì)應(yīng)的profile被激活時(shí),應(yīng)用會(huì)使用嵌入式數(shù)據(jù)源怨喘;當(dāng)qa對(duì)應(yīng)的profile被激活時(shí)津畸,應(yīng)用會(huì)使用DBCP的BasicDataSource;當(dāng)production對(duì)應(yīng)的profile被激活時(shí)必怜,應(yīng)用會(huì)使用從JNDI中獲取的數(shù)據(jù)源肉拓。

上述代碼對(duì)應(yīng)的XML形式的配置代碼如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:jee="http://www.springframework.org/schema/jee" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd">
       <beans profile="qa">
           <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource"
                 p:driverClassName="org.h2.Driver"
                 p:url="jdbc:h2:tcp://localhost/~/spitter"
                 p:username="sa"
                 p:password=""
                 p:initialSize="5" />
       </beans>

       <beans profile="production">
            <jee:jndi-lookup id="dataSource"
                    jndi-name="/jdbc/SpittrDS"
                    resource-ref="true"/>
       </beans>

       <beans profile="development">
            <jdbc:embedded-database id="dataSource" type="H2">
                <jdbc:script location="classpath*:schema.sql" />
                <jdbc:script location="classpath*:test-data.sql" />
            </jdbc:embedded-database>
       </beans>
</beans>

建立好數(shù)據(jù)庫連接后,就可以執(zhí)行訪問數(shù)據(jù)庫的任務(wù)了梳庆。正如之前提到的暖途,Spring對(duì)很多持久化技術(shù)提供了支持,包括JDBC膏执、Hibernate和Java Persistence API(API)驻售。在下一小節(jié)中,我們首先介紹如何在Spring應(yīng)用中使用JDBC書寫持久層更米。

10.3 在Spring應(yīng)用中使用JDBC

在實(shí)際開發(fā)過程中有很多持久化技術(shù)可供選擇:Hibernate欺栗、iBATIS和JPA等。盡管如此征峦,還是有很多應(yīng)用使用古老的方法即JDBC技術(shù)迟几,來訪問數(shù)據(jù)庫。

使用JDBC技術(shù)不需要開發(fā)人員學(xué)習(xí)新的框架栏笆,因?yàn)樗褪腔赟QL語言運(yùn)行的类腮。JDBC技術(shù)更加靈活,開發(fā)人員可以調(diào)整的余地很大竖伯,JDBC技術(shù)允許開發(fā)人員充分利用數(shù)據(jù)庫的本地特性存哲,而在其他ORM框架中可能做不到如此靈活和可定制。

除了上述提到的靈活性七婴、可定制能力祟偷,JDBC技術(shù)也有一些缺點(diǎn)。

10.3.1 分析JDBC代碼

開發(fā)者使用JDBC技術(shù)提供的API可以非常底層得操作數(shù)據(jù)庫打厘,同時(shí)也意味著修肠,開發(fā)者需要負(fù)責(zé)處理數(shù)據(jù)訪問過程中的各個(gè)具體步驟:管理數(shù)據(jù)庫資源和處理數(shù)據(jù)庫訪問異常。如果你使用JDBC插入數(shù)據(jù)庫户盯,在這個(gè)例子中嵌施,假設(shè)需要插入一條spitter數(shù)據(jù),則可以使用如下代碼:

@Component
public class SpitterDao {
    private static final String SQL_INSERT_SPITTER =
            "insert into spitter (username, password, firstName, lastName) values (?, ?, ?, ?)";

    @Autowired
    private DataSource dataSource;

    public void addSpitter(Spitter spitter) {
        Connection conn = null;
        PreparedStatement stmt = null;

        try {
            conn = dataSource.getConnection();
            stmt = conn.prepareStatement(SQL_INSERT_SPITTER);
            stmt.setString(1, spitter.getUsername());
            stmt.setString(2, spitter.getPassword());
            stmt.setString(3, spitter.getFirstName());
            stmt.setString(4, spitter.getLastName());
            stmt.execute();
        } catch (SQLException e) {
            //do something...not sure what, though
        } finally {
            try {
                if (stmt != null) {
                    stmt.close();
                }
                if (conn != null) {
                    conn.close();
                }
            } catch (SQLException e) {
                //I'm even less sure about what to do here
            }
        }
    }
}

addSpitter函數(shù)一共有28行莽鸭,但是只有6行是真正的業(yè)務(wù)邏輯吗伤。為什么如此簡單的操作也需要這么多代碼?JDBC需要開發(fā)者自己管理數(shù)據(jù)庫連接硫眨、自己管理SQL語句足淆,以及自己處理可能拋出的異常。

對(duì)于SQLException,開發(fā)者并不清楚具體該如何處理該異常(該異常并未指明具體的錯(cuò)誤原因)巧号,卻被迫需要捕獲該異常族奢。如果在執(zhí)行插入語句時(shí)發(fā)生錯(cuò)誤,你需要捕獲該異常丹鸿;如果在關(guān)閉statement和connection資源時(shí)發(fā)生錯(cuò)誤越走,你也需要捕獲該異常,但是捕獲后你并不能做實(shí)際的有意義的操作靠欢。

同樣廊敌,如果你需要更新一條spitter記錄,則可使用下列代碼:

private static final String SQL_UPDATE_SPITTER =
        "update spitter set username = ?, password = ?, firstName = ?, lastName=? where id = ?";

public void saveSpitter(Spitter spitter) {
    Connection conn = null;
    PreparedStatement stmt = null;

    try {
        conn = dataSource.getConnection();
        stmt = conn.prepareStatement(SQL_UPDATE_SPITTER);
        stmt.setString(1, spitter.getUsername());
        stmt.setString(2, spitter.getPassword());
        stmt.setString(3, spitter.getFirstName());
        stmt.setString(4, spitter.getLastName());
        stmt.setLong(5, spitter.getId());
        stmt.execute();
    } catch (SQLException e) {
        // Still not sure what I'm supposed to do here
    } finally {
        try {
            if (stmt != null) {
                stmt.close();
            }
            if (conn != null) {
                conn.close();
            }
        } catch (SQLException e) {
            // or here
        }
    }
}

這一次掺涛,saveSpitter函數(shù)用于更新數(shù)據(jù)庫中的一行記錄庭敦,可以看出,有很多重復(fù)代碼薪缆。理想情況應(yīng)該是:你只需要寫特定功能相關(guān)的代碼秧廉。

為了補(bǔ)足JDBC體驗(yàn)之旅,我們?cè)倏纯慈绾问褂肑DBC從數(shù)據(jù)庫中查詢一條記錄拣帽,例子代碼如下:

private static final String SQL_SELECT_SPITTER =
        "select id, username, firstName, lastName from spitter where id = ?";

public Spitter findOne(long id) {
    Connection conn = null;
    PreparedStatement stmt = null;
    ResultSet rs = null;

    try {
        conn = dataSource.getConnection();
        stmt = conn.prepareStatement(SQL_SELECT_SPITTER);
        stmt.setLong(1, id);
        rs = stmt.executeQuery();
        Spitter spitter = null;
        if (rs.next()) {
            spitter = new Spitter();
            spitter.setId(rs.getLong("id"));
            spitter.setUsername(rs.getString("username"));
            spitter.setPassword(rs.getString("password"));
            spitter.setFirstName(rs.getString("firstName"));
            spitter.setLastName(rs.getString("lastName"));
        }
        return spitter;
    } catch (SQLException e) {
    } finally {
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e) { }
        }
        if (stmt != null) {
            try {
                stmt.close();
            } catch (SQLException e) { }
        }
        if (conn != null) {
            try {
                conn.close();
            } catch (SQLException e) { }
        }
    }
    return null;
}

這個(gè)函數(shù)跟之前的insert和update例子一樣啰嗦冗長:幾乎只有20%的代碼是真正有用的業(yè)務(wù)邏輯疼电,而80%的代碼則是模板樣式代碼。

可以看出减拭,使用JDBC持久化技術(shù)蔽豺,就需要編寫大量的模板樣式代碼,用于創(chuàng)建連接拧粪、創(chuàng)建statements和處理異常修陡。另外,上述提到的模板樣式代碼在數(shù)據(jù)庫訪問過程中又非常重要:釋放資源和處理異常等可霎,這都能提高數(shù)據(jù)訪問的穩(wěn)定性魄鸦。如果沒有這些操作,應(yīng)用就無法及時(shí)處理錯(cuò)誤癣朗、資源始終被占用拾因,會(huì)導(dǎo)致內(nèi)存泄露。因此旷余,開發(fā)者需要一個(gè)數(shù)據(jù)庫訪問框架绢记,用于處理這些模板樣式代碼。

10.3.2 使用Spring提供的JDBC模板

Spring提供的JDBC框架負(fù)責(zé)管理資源和異常處理正卧,從而可以簡化開發(fā)者的JDBC代碼蠢熄。開發(fā)者只需要編寫寫入和讀取數(shù)據(jù)庫相關(guān)的代碼即可。

正如在之前的小節(jié)中論述過的炉旷,Spring將數(shù)據(jù)庫訪問過程中的模板樣式代碼封裝到各個(gè)模板類中了护赊,對(duì)于JDBC惠遏,Spring提供了下列三個(gè)模板類:

  • JdbcTemplate——最基本的JDBC模板砾跃,這個(gè)類提供了簡單的接口骏啰,通過JDBC和索引參數(shù)訪問數(shù)據(jù)庫;
  • NameParameterJdbcTemplate——這個(gè)JDBC模板類是的開發(fā)者可以執(zhí)行綁定了指定參數(shù)名稱的SQL抽高,而不是索引參數(shù)判耕;
  • SimpleJdbcTemplate——這個(gè)版本的JDBC模板利用了Java 5的一些特性,例如自動(dòng)裝箱/拆箱翘骂、接口和變參列表等壁熄,用于簡化JDBC模板的使用。

從Spring 3.1開始已經(jīng)將SimpleJdbcTemplate廢棄碳竟,它所擁有的Java 5那些特性被添加到原來的JdbcTemplate中了草丧,因此你可以直接使用JdbcTemplate;當(dāng)你希望在查詢中使用命名參數(shù)時(shí)莹桅,則可以選擇使用NamedParameterJdbcTemplate昌执。

INSERTING DATA USING JDBCTEMPLATE

要使用JdbcTemplate對(duì)象,需要為之傳遞DataSource對(duì)象诈泼。如果使用Java Config配置JdbcTemplatebean懂拾,則對(duì)應(yīng)代碼如下:

@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
}

這里通過構(gòu)造函數(shù)將DataSource對(duì)象注入,而dataSourcebean則來自DataSourceConfiguration文件中定義的javax.sql.DataSource實(shí)例铐达。

然后就可以在自己的repository實(shí)現(xiàn)中注入jdbcTemplatebean岖赋,例如,假設(shè)Spitter的repository使用jdbcTemplatebean瓮孙,代碼可列舉如下:

package org.test.spittr.dao;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.stereotype.Repository;
import org.test.spittr.data.Spitter;

@Repository
public class JdbcSpitterRepository implements SpitterRepository {
    @Autowired
    private JdbcOperations jdbcOperations;
    
    .....    
}

這里JdbcSpitterRepository@Repository注解修飾唐断,component-scanning掃描機(jī)制起作用時(shí)會(huì)自動(dòng)創(chuàng)建對(duì)應(yīng)的bean。按照“面向接口編程”的原則杭抠,我們定義JdbcOperations接口對(duì)應(yīng)的實(shí)例脸甘,而JdbcTemplate實(shí)現(xiàn)了這個(gè)接口,從而使得JdbcSpitterRepository與JdbcTemplate解耦合祈争。

使用JdbcTemplate實(shí)現(xiàn)的addSpitter()方法非常簡單斤程,代碼如下:

public void addSpitter(Spitter spitter) {
    jdbcOperations.update(SQL_INSERT_SPITTER,
            spitter.getUsername(),
            spitter.getPassword(),
            spitter.getFirstName(),
            spitter.getLastName());
}

可以看出,這個(gè)版本的addSpitter十分簡單菩混,不強(qiáng)制開發(fā)者寫任何管理資源和處理異常的代碼忿墅,只有插入語句和對(duì)應(yīng)的參數(shù)。

當(dāng)調(diào)用update()方法時(shí)沮峡,JdbcTemplate獲取一個(gè)連接疚脐、創(chuàng)建一個(gè)statement,并執(zhí)行插入語句邢疙。

JdbcTemplate內(nèi)部捕獲了可能拋出的SQLException異常棍弄,然后轉(zhuǎn)為更具體的數(shù)據(jù)庫訪問異常望薄,并重新拋出。由于Spring的數(shù)據(jù)庫訪問異常都是運(yùn)行時(shí)異常呼畸,開發(fā)者可以自己決定是否捕獲這些異常痕支。

READING DATA WITH JDBCTEMPLATE

使用JdbcTemplate工具從數(shù)據(jù)庫中讀取數(shù)據(jù)也非常簡單,下列代碼展示了改造過后的findOne()函數(shù):調(diào)用JdbctTemplatequeryForObject函數(shù)蛮原,用于通過ID查詢Spitter對(duì)象卧须。

public Spitter findOne(long id) {
    return jdbcOperations.queryForObject(
            SQL_SELECT_SPITTER,
            new SpitterRowMapper(),
            id);
}

private static final class SpitterRowMapper implements RowMapper<Spitter> {
    public Spitter mapRow(ResultSet resultSet, int i) throws SQLException {
        return new Spitter(
                resultSet.getLong("id"),
                resultSet.getString("firstName"),
                resultSet.getString("lastName"),
                resultSet.getString("username"),
                resultSet.getString("password"));
    }
}

findOne()函數(shù)使用JdbcTemplatequeryForObject()方法從數(shù)據(jù)庫中查詢Spitter記錄。queryForObject()方法包括三個(gè)參數(shù):

  • SQL字符串儒陨,用于從數(shù)據(jù)庫中查詢數(shù)據(jù)花嘶;
  • RowMapper對(duì)象,用于從結(jié)果集ResultSet中提取數(shù)據(jù)并構(gòu)造Spitter對(duì)象蹦漠;
  • 變量列表椭员,用于指定查詢參數(shù)(這里是通過id查詢)。

這里需要注意SpitterRowMapper類笛园,它實(shí)現(xiàn)了RowMapper接口隘击,對(duì)于查詢結(jié)果,JdbcTemplate調(diào)用mapRow()方法——一個(gè)ResultSet參數(shù)和一個(gè)row number參數(shù)喘沿。mapRow()方法的主要作用是:從結(jié)果集中取出對(duì)應(yīng)屬性的值闸度,并構(gòu)造一個(gè)Spitter對(duì)象。

addSpitter()方法相同蚜印,findOne()方法也沒有那些JDBC模板樣式代碼莺禁,只有純粹的用于查詢Spitter數(shù)據(jù)的代碼。

10.4 總結(jié)

數(shù)據(jù)就像應(yīng)用的血液窄赋,在某些以數(shù)據(jù)為中心的業(yè)務(wù)中哟冬,數(shù)據(jù)本身就是應(yīng)用。在企業(yè)級(jí)應(yīng)用開發(fā)中忆绰,編寫穩(wěn)定浩峡、簡單、性能良好的數(shù)據(jù)訪問層非常重要错敢。

JDBC是Java處理關(guān)系型數(shù)據(jù)的基本技術(shù)翰灾。原生的JDBC技術(shù)并不完美,開發(fā)者不得不寫很多模板樣式代碼稚茅,用于管理資源和處理異常纸淮。Spring提供了對(duì)應(yīng)的模板工具類,用于消除這些模板樣式代碼亚享。

后記:最近在項(xiàng)目開發(fā)中咽块,遇到一次高并發(fā)下數(shù)據(jù)庫成為性能瓶頸的情況,對(duì)數(shù)據(jù)訪問層的各個(gè)階段有了深入的了解:建立數(shù)據(jù)庫連接欺税、轉(zhuǎn)換SQL語句侈沪、執(zhí)行SQL語句揭璃、獲取執(zhí)行結(jié)果、釋放資源亭罪。我們?cè)陧?xiàng)目開發(fā)中使用的數(shù)據(jù)庫連接池是德魯伊(DruidDataSource)瘦馍,它的配置跟DBCP類似,在實(shí)際開發(fā)中皆撩,我們需要理解每個(gè)配置項(xiàng)的含義扣墩,用于性能調(diào)優(yōu)。后續(xù)我會(huì)寫一篇關(guān)于數(shù)據(jù)庫連接池的文章扛吞。
另外,我們現(xiàn)在開發(fā)中最常用的是Mybatis框架荆责,具體內(nèi)容可以參考《Java Persistence With Mybaits 3》一書滥比,也可以參考Mybatis-Spring教程(中文版)


本號(hào)專注于后端技術(shù)、JVM問題排查和優(yōu)化做院、Java面試題盲泛、個(gè)人成長和自我管理等主題,為讀者提供一線開發(fā)者的工作和成長經(jīng)驗(yàn)键耕,期待你能在這里有所收獲寺滚。


javaadu
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市屈雄,隨后出現(xiàn)的幾起案子村视,更是在濱河造成了極大的恐慌,老刑警劉巖酒奶,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蚁孔,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡惋嚎,警方通過查閱死者的電腦和手機(jī)杠氢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來另伍,“玉大人鼻百,你說我怎么就攤上這事“诔ⅲ” “怎么了温艇?”我有些...
    開封第一講書人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長结榄。 經(jīng)常有香客問我中贝,道長,這世上最難降的妖魔是什么臼朗? 我笑而不...
    開封第一講書人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任邻寿,我火速辦了婚禮蝎土,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘绣否。我一直安慰自己誊涯,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開白布蒜撮。 她就那樣靜靜地躺著暴构,像睡著了一般。 火紅的嫁衣襯著肌膚如雪段磨。 梳的紋絲不亂的頭發(fā)上取逾,一...
    開封第一講書人閱讀 52,457評(píng)論 1 311
  • 那天,我揣著相機(jī)與錄音苹支,去河邊找鬼砾隅。 笑死,一個(gè)胖子當(dāng)著我的面吹牛债蜜,可吹牛的內(nèi)容都是我干的晴埂。 我是一名探鬼主播,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼寻定,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼儒洛!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起狼速,我...
    開封第一講書人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤琅锻,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后唐含,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體浅浮,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年捷枯,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了滚秩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡淮捆,死狀恐怖郁油,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情攀痊,我是刑警寧澤桐腌,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站苟径,受9級(jí)特大地震影響案站,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜棘街,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一蟆盐、第九天 我趴在偏房一處隱蔽的房頂上張望承边。 院中可真熱鬧,春花似錦石挂、人聲如沸博助。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽富岳。三九已至,卻和暖如春拯腮,著一層夾襖步出監(jiān)牢的瞬間窖式,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來泰國打工疾瓮, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留脖镀,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓狼电,卻偏偏與公主長得像,于是被迫代替她去往敵國和親弦蹂。 傳聞我的和親對(duì)象是個(gè)殘疾皇子肩碟,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)凸椿,斷路器削祈,智...
    卡卡羅2017閱讀 134,707評(píng)論 18 139
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,859評(píng)論 6 342
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,304評(píng)論 25 707
  • 喜歡周瑩身上那股子傲氣,桀驁不馴脑漫∷枰郑看到說到,眼神里透著一股子光优幸,她不在意世界給了她什么吨拍,只有養(yǎng)父如何,養(yǎng)父賣了自己...
    該南閱讀 256評(píng)論 0 0
  • 1队秩、 “今年的畢業(yè)季風(fēng)云人物專訪,校報(bào)居然讓我這個(gè)退隱江湖多年的老部長重操舊業(yè)昼浦!”一踏進(jìn)宿舍馍资,我壓了一路的火氣順著...
    南城詩爺閱讀 650評(píng)論 0 1