Spring 是分層的 Java SE/EE 應用 full-stack 輕量級開源框架,以 IoC(Inverse Of Control)控制反轉 和 AOP(Aspect Oriented Programming)面向切面編程爲內核,提供了展示層 Spring MVC 和持久層 Spring JDBC 以及業務層事務管理等技術,還能整合開源的第三方框架和類庫,是使用最多的 Java EE 企業應用開源框架。java
咱們日常說的 Spring 指的是 Spring Framework,其爲 Java 程序提供全面的基礎架構支持,Spring 處理基礎結構,使得咱們能夠專一於業務自己。是非入侵式框架(導入項目不會破壞原有項目代碼)mysql
Spring 之父:Rod Johnson程序員
Spring Framework 由組成大約 20 個模塊的 feature 組成,這些模塊分爲:web
Core Container 核心容器正則表達式
Date Access/Integration 數據訪問/整合spring
Websql
AOP (Aspect Oriented Programming) 面向切面編程數據庫
Instrumentation 檢測express
Messaging 消息apache
Test
在面向對象編程中,咱們常常處理的問題就是解耦,程序的耦合性越低,表名這個程序的可讀性以及可維護性越高。IoC(Inversion of Control) 控制反轉,就是經常使用的面向對象編程的設計原則,使用這個原則咱們能夠下降耦合性。其中依賴注入是控制反轉最多見的實現。
耦合
程序間的依賴關係
包括:類之間的依賴
方法之間的依賴
解耦
下降程序間的依賴關係
咱們在開發中,有些依賴關係是必須的,有些依賴關係能夠經過優化代碼來解除的。
因此實際開發中應作到:編譯期不依賴,運行時才依賴
解耦思路:
範例:JDBC 鏈接數據庫
public static void main(String[] args) throws Exception{ //1.註冊驅動 /*使用new對象的方式註冊驅動 DriverManager.registerDriver(new com.mysql.jdbc.Driver());*/ /*使用反射方式建立對象註冊驅動,此時配置內容只做爲一個字符串傳遞 Class.forName("com.mysql.jdbc.Driver");*/ //而經過讀取配置文件的方式,解決上面將字符串在代碼中寫死的問題,便於修改配置 Properties properties = new Properties(); properties.load(new FileInputStream("src/main/resources/data.properties")); //略... //2.獲取鏈接 Connection conn = DriverManager.getConnection(url,user,password); //3.獲取操做數據庫的預處理對象 PrepareStatement ps = conn.prepareStatement("select * from tb_students"); //4.執行SQL,獲取結果集 result = ps.executeQuery(); //5.遍歷結果集 while(result.next()){ int no = result.getInt("no"); String name = result.getString("name"); System.out.println(no + "," + name); //6.釋放資源 result.close(); ps.close(); conn.close(); }
傳統的 JDBC 獲取鏈接方式也是爲了解耦而使用讀取配置文件的方式配置數據源。
耦合性(Coupling),也叫耦合度,是對模塊間關聯程度的度量。耦合的強弱取決於模塊間接口的複雜性、調 用模塊的方式以及經過界面傳送數據的多少。模塊間的耦合度是指模塊之間的依賴關係,包括控制關係、調用關係、數據傳遞關係。模塊間聯繫越多,其耦合性越強,同時代表其獨立性越差( 下降耦合性,能夠提升其獨立 性)。耦合性存在於各個領域,而非軟件設計中獨有的,可是咱們只討論軟件工程中的耦合。
在軟件工程中,耦合指的就是就是對象之間的依賴性。對象之間的耦合越高,維護成本越高。所以對象的設計應使類和構件之間的耦合最小。軟件設計中一般用耦合度和內聚度做爲衡量模塊獨立程度的標準。劃分模塊的一個 準則就是高內聚低耦合
它有以下分類:
(1) 內容耦合。當一個模塊直接修改或操做另外一個模塊的數據時,或一個模塊不經過正常入口而轉入另 一個模塊時,這樣的耦合被稱爲內容耦合。內容耦合是最高程度的耦合,應該避免使用之。 (2) 公共耦合。兩個或兩個以上的模塊共同引用一個全局數據項,這種耦合被稱爲公共耦合。在具備大 量公共耦合的結構中,肯定到底是哪一個模塊給全局變量賦了一個特定的值是十分困難的。 (3) 外部耦合 。一組模塊都訪問同一全局簡單變量而不是同一全局數據結構,並且不是經過參數表傳 遞該全局變量的信息,則稱之爲外部耦合。
(4) 控制耦合 。一個模塊經過接口向另外一個模塊傳遞一個控制信號,接受信號的模塊根據信號值而進 行適當的動做,這種耦合被稱爲控制耦合。
(5) 標記耦合 。若一個模塊 A 經過接口向兩個模塊 B 和 C 傳遞一個公共參數,那麼稱模塊 B 和 C 之間 存在一個標記耦合。
(6) 數據耦合。模塊之間經過參數來傳遞數據,那麼被稱爲數據耦合。數據耦合是最低的一種耦合形 式,系統中通常都存在這種類型的耦合,由於爲了完成一些有意義的功能,每每須要將某些模塊的輸出數據做爲另
一些模塊的輸入數據。 (7) 非直接耦合 。兩個模塊之間沒有直接關係,它們之間的聯繫徹底是經過主模塊的控制和調用來實 現的。
總結: 耦合是影響軟件複雜程度和設計質量的一個重要因素,在設計上咱們應採用如下原則:若是模塊間必須 存在耦合,就儘可能使用數據耦合,少用控制耦合,限制公共耦合的範圍,儘可能避免使用內容耦合。
內聚與耦合
內聚標誌一個模塊內各個元素彼此結合的緊密程度,它是信息隱蔽和局部化概念的天然擴展。內聚是從 功能角度來度量模塊內的聯繫,一個好的內聚模塊應當剛好作一件事。它描述的是模塊內的功能聯繫。耦合是軟件結構中各模塊之間相互鏈接的一種度量,耦合強弱取決於模塊間接口的複雜程度、進入或訪問一個模塊的點以及經過接口的數據。 程序講究的是低耦合,高內聚。就是同一個模塊內的各個元素之間要高度緊密,可是各個模塊之 間的相互依存度卻要不那麼緊密。
內聚和耦合是密切相關的,同其餘模塊存在高耦合的模塊意味着低內聚,而高內聚的模塊意味着該模塊同其餘模塊之間是低耦合。在進行軟件設計時,應力爭作到高內聚,低耦合
具體到項目中,帶來了哪些依賴問題呢:
先了解一下工廠模式解耦的思想,會給下面 Spring 控制反轉使用帶來啓發。
在實際開發中咱們能夠把三層的對象都使用配置文件配置起來,當啓動服務器應用加載的時候,讓一個類中的方法經過讀取配置文件,把這些對象建立出來並存起來。在接下來的使用的時候,能夠直接拿過來用。
那麼,這個讀取配置文件,建立和獲取三層對象的類就是工廠(Factory)
範例:
項目結構:
對應代碼:以表現層 - 業務層 - 持久層 - 工廠 順序
表現層代碼:
package com.yh.view; import com.yh.factory.BeanFactory; import com.yh.service.INameService; /** * 模擬一個表現層用於調用業務層 * @author YH * @create 2020-05-07 16:19 */ public class Cilent { public static void main(String[] args){ //想調用業務層方法依賴與其實現類對象 // INameService service = new NameServiceImpl(); INameService service = (INameService)BeanFactory.getBean("nameService"); System.out.println("表現層後臺代碼執行調用業務邏輯層:1"); service.method(); } }
業務層代碼:
package com.yh.service; /** * 業務邏輯層接口 * @author YH * @create 2020-05-07 16:17 */ public interface INameService { void method(); }
package com.yh.service.impl; import com.yh.dao.INameDao; import com.yh.dao.impl.NameDaoImpl; import com.yh.factory.BeanFactory; import com.yh.service.INameService; /** * 模擬業務邏輯層調用持久層 * @author YH * @create 2020-05-07 16:18 */ public class NameServiceImpl implements INameService { @Override public void method() { //想調用持久層方法依賴與其實現類對象 // INameDao nameDao = new NameDaoImpl(); INameDao nameDao = (INameDao) BeanFactory.getBean("nameDao"); System.out.println("業務邏輯層實現類執行調用持久層:2"); nameDao.method(); } }
持久層代碼:
package com.yh.dao; /** * 持久層接口 * @author YH * @create 2020-05-07 16:14 */ public interface INameDao { void method(); }
package com.yh.dao.impl; import com.yh.dao.INameDao; /** * 模擬持久層 * @author YH * @create 2020-05-07 16:15 */ public class NameDaoImpl implements INameDao { @Override public void method() { System.out.println("持久層dao執行 3"); } }
工廠:
package com.yh.factory; import java.io.InputStream; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import java.util.Properties; /** * 一個建立Bean對象的工廠 * Bean:在計算機英語中,有可重用組件的含義 * JavaBean:用java語言編寫的可重用組件 * 注意:JavaBean不等於實體類,且包含實體類,即 JavaBean > 實體類 * * 建立service和dao對象 * * 1.須要經過配置文件讀取配置,可用兩種方式: xml 或 properties * 配置的內容:惟一標識=全限定類名(key-value) * 2.再經過讀取配置文件中配置的內容,反射建立對象 * @author YH * @create 2020-05-07 17:14 */ public class BeanFactory { private static Properties props; /** * 定義一個Map,做爲存儲對象的容器,存放咱們要建立的對象 */ private static Map<String,Object> beans = null; /** * 靜態代碼塊只執行一次,保證了從始至終只生成配置中對應的惟一一個實例 */ static { try { props = new Properties(); InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("factory.properties"); props.load(in); //實例化Map容器 beans = new HashMap<>(); //取出配置文件中全部的key Enumeration<Object> keys = props.keys(); //遍歷枚舉 while(keys.hasMoreElements()){ //取出每一個key String key = keys.nextElement().toString(); //根據key從配置中讀取value String beanPath = props.getProperty(key); //反射建立實例對象 Object value = BeanFactory.class.forName(beanPath).newInstance(); //把key和value存入容器中 beans.put(key,value); } } catch (Exception e) { //讀取配置文件出現異常那麼後面的操做都無心義,因此直接聲明一個錯誤終止程序 throw new ExceptionInInitializerError("初始化properties時發生錯誤!"); } } /** * 根據bean的名稱獲取bean對象 * @param beanName * @return */ public static Object getBean(String beanName){ return beans.get(beanName); } /** * 傳入key的名稱尋找對應的value全類名 並建立對象返回 * @param beanName * @return *//* public static Object getBean(String beanName){ Object bean = null; try { String beanPath = props.getProperty(beanName); //每次都會調用默認構造函數建立對象 bean = (Object) Class.forName(beanPath).newInstance(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } return bean; }*/ }
小結:經過工廠類初始化加載就將配置文件中所表明的類建立並存儲到 Map 中,須要使用時調用工廠方法便可,避免了 new,即避免了反覆建立對象,也下降了程序的耦合度
控制反轉(Inversion Of Control)把建立對象的權利交給框架,是框架的重要特徵,並不是面向對象編程的專用術語。它包括依賴注入(DI)和依賴查找(DL)
做用:消減計算機程序的耦合(解除咱們代碼中的依賴關係)
以上面小節爲例:
咱們經過工廠建立對象,將對象存儲在容器中,提供獲取對象的方法。在這個過程當中:
獲取對象的方式發生了改變:
之前:獲取對象,採用 new 的方式,是主動的
如今:經過工廠獲取對象,工廠爲咱們查找或者建立對象,是被動的
準備 spring 的開發包
以上一節工廠解耦改成使用 spring
第一步:向項目的 pro.xml 文件中加入配置,將 spring 的 jar 包導入工程:
<!--設置打包方式--> <packaging>jar</packaging> <dependencies> <!-- 導入spring--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.0.2.RELEASE</version> </dependency> </dependencies>
第二步:在資源目錄下建立一個 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" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- bean標籤:用於配置讓spring建立對象,而且存入IOC容器之中 id屬性:對象的惟一標識 class屬性:指定要建立對象的全限定類名 --> <bean id="dao" class="yh.dao.impl.NameDaoImpl"></bean> <bean id="service" class="yh.service.impl.NameServiceImpl"></bean> </beans>
第三步:讓 spring 管理資源,在配置文件中配置 service 和 dao
public class Client { /** * 獲取spring的核心容器 並根據id獲取對象 * @param args */ public static void main(String[] args){ //1.獲取核心容器對象 ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml"); //2.根據id獲取bean對象 INameDao dao = (INameDao)ac.getBean("dao"); INameService service = ac.getBean("service",INameService.class); System.out.println(dao); System.out.println(service); } }
測試配置是否成功:
Spring 中工廠的類結構:
能夠看出 BeanFactory 是 Spring 容器中的頂層接口,ApplicationContext 是其子接口,它們建立對象的時間點的區別:
ApplicationContext:只要一讀取配置文件,默認狀況下就會建立對象(即時建立),能夠推斷:即時建立對象適合使用在單例模式的場景,對象只建立一次
BeanFactory:何時使用對象了,纔會建立對象(延遲建立),同理:延遲建立對象適合於多例模式的場景,節省性能開銷
ApplicationContext 的三個經常使用實現類:
控制反轉 IoC,是一種設計思想,DI(依賴注入)是實現 IoC 的一種方法。沒有 IoC 的程序中,使用面向對象編程,對象的建立與對象的依賴關係徹底硬編碼在程序中,對象的建立由程序本身控制,控制反轉後將對象的建立轉移給第三方,即得到依賴對象的方式反轉了。
IoC 是 Spring 框架的核心內容,使用多種方式完美實現了 IoC,可使用 XML 配置,也可使用註解,新版本的 Spring 也能夠零配置實現 IoC。Spring 容器在初始化時先讀取配置文件,根據配置文件或元數據建立與組織對象存入容器中,程序使用時再從 IoC 容器中取出須要的對象。
控制反轉是一種經過描述(XML 或註解)並經過第三方去生產或獲取特定對象的方式,在 Spring 中實現控制反轉的是 IoC 容器,其實現方法是依賴注入(Dependency Injection,DI)。
所謂控制反轉,就是應用自己不負責依賴對象的建立及維護,依賴對象的建立及維護是由外部容器負責的。其中依賴注入是控制反轉最多見的實現。
那咱們來先搞清這個依賴對象是什麼,下面是傳統三層架構的代碼示例:
持久層:
//持久層接口 public interface IUserDao { void daoMethod(); }
//持久層接口實現1 public class UserDaoImpl implements IUserDao { public void daoMethod() { System.out.println("數據庫鏈接1"); } }
//持久層接口實現2 public class UserDaoImpl2 implements IUserDao { public void daoMethod() { System.out.println("數據庫鏈接2"); } }
持久層即數據訪問層(DAL 層),其功能主要是負責數據庫的訪問,實現對數據表的 CEUD 等操做。
可能會有變動接口實現的需求(如 MySQL 換爲 Oracle)
業務邏輯層:
//業務邏輯層接口 public interface IUserService { void serviceMethod(); }
//業務邏輯層接口實現 public class UserServiceImpl implements IUserService { //業務層須要或許持久層對象,調用其方法 IUserDao dao = new UserDaoImpl(); public void serviceMethod() { dao.daoMethod(); } }
三層架構的核心,其關注點是業務規則的制定、業務流程的實現等與業務需求有關的系統設計。
視圖層(表示層):
@Test public void test1(){ //程序入口要獲取業務層對象來調用功能 IUserService service = new UserServiceImpl(); service.serviceMethod(); }
表示層主要做用是與用戶進行交互,顯示數據(如打印到控制檯的信息)和接收傳輸用戶的數據,提供用戶操做界面等。
運行結果:
這就是傳統三層架構的一個調用流程,能夠看出做爲三層核心的業務層起的一個承上啓下的做用。表示層與用戶交互,要執行功能那麼就須要先貨到控制層的對象,調用相關功能。即沒有業務層對象就無法實現操做,則表示層依賴於業務邏輯層,沒它不行;一樣的,業務邏輯層做爲一個指揮全局的頭,須要指揮小弟來辦事,因此他先得有個小弟,那麼就獲取一個持久層對象了,一樣是沒有這個小弟無法辦事,並且加入要辦另一件事須要另外一個小弟,那業務層大哥也要作相應的調整(改代碼)。此時業務邏輯層依賴於持久層。
真是世間美好與你環環相扣,變強了,頭也就禿了(手動**)
針對變動持久層實現須要修改業務層代碼的問題作一個優化,使用 set 方法注入方式獲取對象,以下:
業務層實現類:
public class UserServiceImpl implements IUserService { /** * 對象注入 */ private IUserDao dao; public void set(IUserDao dao){ this.dao = dao; } public void serviceMethod() { dao.daoMethod(); } }
利用多態的特性可接收任何其實現對象,外部根據不一樣的需求傳遞不一樣的實現對象參數,從而避免了二次修改業務層代碼。
測試代碼:
@Test public void test1(){ //程序入口要獲取業務層對象來調用功能 IUserService service = new UserServiceImpl(); // service.setDaoImpl(new UserDaoImpl()); service.setDaoImpl(new UserDaoImpl2()); service.serviceMethod(); }
傳入不一樣的實現參數,獲取不一樣的鏈接:
對比:
以前,程序主動建立對象,由程序員決定使用的功能(更改代碼)
使用 set 注入後,程序變成被動接受對象,由使用者決定使用的功能(傳遞對應的參數)
這種讓程序員再也不管理對象建立的思想,使得程序系統的耦合性大大下降,讓程序員能夠更加專一於業務的實現上,這就是 IoC 的原型。
對,是原型,起關鍵做用的就是 set 方法,它是得以注入的關鍵,下面就使用 Spring IoC 來創建第一個程序:
JavaBean:
public class Hello { private String name; //注意此set方法 public void setName(String name){ this.name = name; } public void run(){ System.out.println("Hello!" + name); } }
使用 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" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!--配置元數據 bean的配置: 使用Spring建立對象,對象都用Bean表示 對比原有的new建立對象方式: Heelo hello = new Hello() 即 類型 變量名 = new 類型() - id 指定對象變量名 -> 變量名 - class 指定要建立的對象的類 - property 指定對象的屬性 name 指定屬性名 value 指定屬性值 --> <bean id="hello" class="yh.pojo.Hello"> <property name="name" value="熊大"/> </bean> </beans>
測試代碼:
@Test public void test1(){ //獲取Spring的上下文對象 ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); //咱們的對象如今都在spring中管理了,咱們要使用,直接去裏面取出來便可(取Bean) Hello hello = context.getBean("hello", Hello.class); //Hello hello = (Hello)context.getBean("hello"); hello.run(); }
結果:
整個過程當中:
hello 對象有 Spring 建立
hello 對象的屬性也由 Spring 容器設置
這就是控制反轉:
控制:傳統程序的對象是由程序自己控制建立的,使用 Spring 後,對象是由 Spring 建立的。
反轉:程序自己不建立對象,而變成被動地接收對象。
依賴注入:就是利用 set 方法進行注入。
IOC 就是一種編程思想,由主動的編程編編程被動的接收。
至此,咱們完全不用去程序中改動了,要實現不一樣的操做,只須要在 xml 配置文件中進行修改,對象由 Spring 來建立、管理、裝配。
如今咱們來修改最開始的那個傳統實例,看看用 IoC 如何實現它:
配置文件:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="daoImpl" class="yh.dao.impl.UserDaoImpl"/> <bean id="daoImpl2" class="yh.dao.impl.UserDaoImpl2"/> <bean id="service" class="yh.service.impl.UserServiceImpl"> <!-- ref:引用spring容器中建立好的對象 value:具體的值類型數據 --> <property name="dao" ref="daoImpl"/> </bean> </beans>
因爲業務層實現中本來就設置了 set 方法,因此能夠直接配置注入屬性的信息
注意:set 方法命名必定要按照規範,不然沒法識別注入
其餘地方都不用修改,直接進行測試:
@Test public void test1(){ ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); IUserService service = context.getBean("service", IUserService.class); service.serviceMethod(); }
結果:
如需更改配置,直接修改配置文件中的 dao
屬性值(配置文件不屬於程序代碼),以下:
<bean id="service" class="yh.service.impl.UserServiceImpl"> <!--更改引用值--> <property name="dao" ref="daoImpl2"/> </bean>
增長實現,或換實現原均可以經過元數據完成了。
做用:用於配置對象讓 spring 來建立
默認狀況下他調用的是類中的無參構造器,若是沒有無參構造器則不能建立成功
屬性:
id:給對象在容器中提供一個惟一標識,用於獲取對象
class:指定類的全限定類名,用於反射建立對象。默認狀況下調用無參構造器
scope:指定對象的做用範圍
init-method:指定類中的初始化方法名稱
destroy-method:指定類中銷燬方法名稱
第一種方式:使用構造器
<!--在默認狀況下: 他會根據默認無參構造函數來建立類對象,若是bean中沒有默認無參構造函數,將會建立失敗--> <bean id="service" class="yh.service.impl.NameServiceImpl"></bean>
<bean id="hello" class="yh.pojo.Hello"> <constructor-arg name="name" value="Spring"/> </bean>
第二種方式:spring管理實例工廠,使用實例工廠的方法建立對象
<!--先把工廠的建立交給spring來管理,而後使用工廠bean來調用裏面的方法(先建立工廠對象,再用其獲取service對象) factory-bean 屬性:用於指定實例工廠bean的id factory-method 屬性:用於指定實例工廠中建立對象的方法 --> <bean id="instanceFactory" class="yh.factory.InstanceFactory"></bean> <bean id="nameService" factory-bean="instanceFactory" factory-method="createNameService"></bean>
第三種方式:spring管理靜態工廠,使用靜態工廠的方法建立對象
<!--使用StaticFactory類中的靜態方法建立對象,並存入spring容器 id:指定bean的id,用於從容器中獲取 class:指定靜態工廠的全限定類名 factory-method 屬性:指定生成對象的工廠靜態方法 --> <bean id="nameService" class="yh.factory.StaticFactory" factory-method="createNameService"></bean>
調用類:
package yh.view; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import yh.service.INameService; /** * 模擬一個表現層用於調用業務層 * @author YH * @create 2020-05-07 16:19 */ public class Client { /** * 獲取spring的核心容器 並根據id獲取對象 * @param args */ public static void main(String[] args){ //1.獲取核心容器對象 ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml"); //2.根據id獲取bean對象 INameService service = ac.getBean("nameService",INameService.class); System.out.println(service); } }
測試結果都能獲取到對象:
在配置文件加載的時候,容器中管理的對象(Bean)就已經初始化了,須要哪一個對象經過 Spring 上下文對象直接獲取便可(getBean()
)。
參考官方介紹:
範圍 | 描述 |
---|---|
singleton | (默認)爲每一個 Spring IoC 容器的單個 object 實例定義單個 bean 定義。 |
prototype | 爲任意數量的 object 實例定義單個 bean 定義。 |
request | 將單個 bean 定義範圍限定爲單個 HTTP 請求的生命週期。也就是說,每一個 HTTP 請求都有本身的 bean 實例,該實例是在單個 bean 定義的後面建立的。僅在 web-aware Spring ApplicationContext 的 context 中有效。 |
session | 將單個 bean 定義範圍限定爲 HTTP Session 的生命週期。僅在 web-aware Spring ApplicationContext 的 context 中有效。 |
application | 將單個 bean 定義範圍限定爲ServletContext 的生命週期。僅在 web-aware Spring ApplicationContext 的 context 中有效。 |
websocket | 將單個 bean 定義範圍限定爲WebSocket 的生命週期。僅在 web-aware Spring ApplicationContext 的 context 中有效。 |
bean 對象的做用範圍:
使用 scope 屬性指定對象的做用範圍,參數:
singleton:單例的(默認值)
prototype:多例的
request:WEB 項目中 Spring 建立一個 Bean 的對象,將對象存入到 request 域中
session:WEB 項目中 Spring 建立一個 Bean 的對象,將對象存入到 session 域中
global session:做用於集羣環境的會話範圍(全局會話範圍),不是集羣它就是 session
global session(全局變量)應用場景:
一個web工程可能有多個服務器分流,用戶首次發送請求訪問 web 時所鏈接的服務器和提交登陸所請求的服務器可能不一同一個服務器,可是驗證碼生成首先是從第一次訪問時的服務器獲取的,並保存在獨有 session 中,提交登陸時確定須要比較驗證碼正確性,因爲可能不在一個服務器沒法驗證,因此就須要 global session 這個全局變量,不管在哪一個服務器均可以驗證
示意圖:
生命週期:
單例對象:scope="singleton"
一個應用只有一個對象的實例,它的做用範圍就是整個應用
對象出生:當應用加載,建立容器時,對象就被建立了
對象活着:只要容器在,對象一直活着
對象死亡:當應用卸載,容器銷燬時,對象也被銷燬
多例對象:scope="prototype"
每次訪問時,都會從新建立對象實例
對象出生:當使用對象時,建立新的對象實例
對象活着:對象使用期間一直活着
對象死亡:當對象長時間不用,被java的垃圾回收機制回收了
依賴注入:Dependdency Injection。它是 spring 框架核心 IOC 的具體實現
咱們的程序在編寫時,經過控制反轉,把對象的建立交給了 spring,可是代碼中不可能出現沒有依賴的狀況。ioc 解耦只是下降他們的依賴關係,但不會消除。例如:咱們的業務層仍會調用持久層的方法。
那這種業務層和持久的依賴關係,在使用 spring 以後,就讓 spring 來維護了;
簡單的說,就是坐等框架把持久層對象傳入業務層,而不用咱們本身去獲取
顧名思義,就是使用類中的構造函數,給成員變量賦值
要求:
類中須要提供一個對應的帶參構造器
涉及的標籤:
constructor-arg
屬性:
index:指定要注入的數據給構造函數中指定索引位置的參數賦值,索引從0開始
type:指定要注入數據的數據類型,該類型也是某個或某些參數的類型
name:指定給構造器中指定名稱的參數賦值
---------------以上三個屬性用於指定要給哪一個參數賦值---------------
value:用於提供基本類型和String類型的數據
ref:用於指定其餘的bean類型數據(即在spring的IOC核心容器中出現過的bean對象
- 優點:
在獲取bean對象時,注入數據時必須的操做,不然對象沒法建立成功
- 弊端:
改變了bean對象的實例化方式,調用有參構造器,使咱們在建立對象時,無論需不須要這些數據,也必須提供
xml 文件配置:
<!--使用構造函數的方式,給service中的屬性傳值--> <bean id="nameService" class="yh.service.impl.NameServiceImpl"> <constructor-arg name="name" value="雲翯"></constructor-arg> <constructor-arg name="age" value="18"></constructor-arg> <constructor-arg name="birthday" ref="now"></constructor-arg> </bean> <!-- 配置一個日期對象--> <bean id="now" class="java.util.Date"></bean>
實現類提供有參構造器:
public class NameServiceImpl implements INameService { private String name; private Integer age; private Date birthday; public NameServiceImpl(String name, Integer age, Date birthday) { this.name = name; this.age = age; this.birthday = birthday; } @Override public void method() { System.out.println(name + "," + age + "," + birthday); } }
調用類:
public class Client { /** * 獲取spring的核心容器 並根據id獲取對象 * @param args */ public static void main(String[] args){ //1.獲取核心容器對象 ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml"); //2.根據id獲取bean對象 INameService service = ac.getBean("nameService",INameService.class); service.method(); } }
測試結果:
涉及的標籤:property
出現的位置:bean標籤內部
屬性:
name:指定所用的set方法名稱
value:指定基本類型和String類型的數據
ref:指定其餘bean類型數據(即spring的IOC核心容器中出現過的bean對象)
顧名思義,實現類中須要提供set方法。範例:
xml 配置文件:
<bean id="nameService1" class="yh.service.impl.NameServiceImpl1"> <property name="name" value="雲翯1"></property> <property name="age" value="19"></property> <property name="birthday" ref="now"></property> </bean> <!-- 配置一個日期對象--> <bean id="now" class="java.util.Date"></bean>
帶有 set() 方法的實現類:
public class NameServiceImpl1 implements INameService { private String name; private Integer age; private Date birthday; public NameServiceImpl1() {} public void setName(String name) { this.name = name; } public void setAge(Integer age) { this.age = age; } public void setBirthday(Date birthday) { this.birthday = birthday; } @Override public void method() { System.out.println(name + "," + age + "," + birthday); } }
調用類:
public class Client { /** * 獲取spring的核心容器 並根據id獲取對象 * @param args */ public static void main(String[] args){ //1.獲取核心容器對象 ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml"); //2.根據id獲取bean對象 INameService service = ac.getBean("nameService1",INameService.class); service.method(); } }
測試結果:
注入集合數據(複雜類型注入)
顧名思義,就是給集合成員傳值,他用的也是set方法注入的方式,只不過變量的數據類型都是集合。
用於給 List 結構集合注入的標籤
list、array、set
用於給 Map 結構集合注入的標籤
map、props
結構相同,標籤能夠互用
範例:
xml 配置文件:
<!-- 複雜類型的注入/集合類型的注入--> <bean id="nameService2" class="yh.service.impl.NameServiceImpl3"> <property name="myStrs"> <array> <value>AAA</value> <value>BBB</value> <value>CCC</value> </array> </property> <property name="myList"> <list> <value>AAA</value> <value>BBB</value> <value>CCC</value> </list> </property> <property name="mySet"> <set> <value>AAA</value> <value>BBB</value> <value>CCC</value> </set> </property> <property name="myMap"> <map> <entry key="testA" value="aaa"></entry> <entry key="testB" value="bbb"></entry> <entry key="testC" value="ccc"></entry> </map> </property> <property name="myProps"> <props> <prop key="testA">aaa</prop> <prop key="testB">bbb</prop> </props> </property> </bean>
集合等複雜類型的屬性,一樣使用set方法賦值:
public class NameServiceImpl3 implements INameService { private String[] myStrs; private List<String> myList; private Set<String> mySet; private Map<String,String> myMap; private Properties myProps; public void setMyStrs(String[] myStrs) { this.myStrs = myStrs; } public void setMyList(List<String> myList) { this.myList = myList; } public void setMySet(Set<String> mySet) { this.mySet = mySet; } public void setMyMap(Map<String,String> myMap) { this.myMap = myMap; } public void setMyProps(Properties myProps) { this.myProps = myProps; } @Override public void method() { System.out.println(myStrs); System.out.println(myList); System.out.println(mySet); System.out.println(myMap); System.out.println(myProps); } }
調用類:
public class Client { /** * 獲取spring的核心容器 並根據id獲取對象 * @param args */ public static void main(String[] args){ //1.獲取核心容器對象 ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml"); //2.根據id獲取bean對象 INameService service = ac.getBean("nameService2",INameService.class); service.method(); } }
測試結果:
咱們可使用 p 命名空間和 c 命名空間,進行注入
<?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:c="http://www.springframework.org/schema/c" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!--p命名空間注入,能夠直接注入屬性的值--> <bean id="user" class="yh.pojo.User" p:name="無問西東" p:age="18"/> <!--c命名空間注入,能夠直接注入構造器的值--> <bean id="user2" class="yh.pojo.User" c:name="無問西東" c:age="18"/> </beans>
p 和 c 命名空間容許 bean 元素經過屬性(而不是嵌套的子元素)來描述注入的屬性值。可是不能直接使用,須要導入 XML 約束:
xmlns:p="http://www.springframework.org/schema/p" xmlns:c="http://www.springframework.org/schema/c"
結構:
Account 類:
public class Account { private int id; private String name; private float money; //標準JavaBean,剩餘代碼略... }
dao 接口:
public interface IAccountDao { /** * 查詢全部 * @return */ List<Account> findAccounts(); /** * 查詢一個 * @param account * @return */ Account findAccountById(Integer account); /** * 保存 * @param account */ void saveAccount(Account account); /** * 更新 * @param account */ void updateAccount(Account account); /** * 刪除 * @param id */ void deleteAccountById(Integer id); }
dao 接口實現:
public class AccountDaoImpl implements IAccountDao { private QueryRunner runner; public void setRunner(QueryRunner runner) { this.runner = runner; } public List<Account> findAccounts() { try { return runner.query("select * from account",new BeanListHandler<Account>(Account.class)); } catch (Exception e) { throw new RuntimeException(e); } } public Account findAccountById(Integer account) { try { return runner.query("select * from account where id=?",new BeanHandler<Account>(Account.class),account); } catch (Exception e) { throw new RuntimeException(e); } } public void saveAccount(Account account) { try { runner.update("insert into account(name,money) values(?,?)",account.getName(),account.getMoney()); } catch (Exception e) { throw new RuntimeException(e); } } public void updateAccount(Account account) { try { runner.update("update account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId()); } catch (Exception e) { throw new RuntimeException(e); } } public void deleteAccountById(Integer id) { try { runner.update("delete from account where id=?",id); } catch (Exception e) { throw new RuntimeException(e); } } }
service 層:
public interface IAccountService { /** * 查詢全部 * @return */ List<Account> findAccounts(); /** * 查詢一個 * @param account * @return */ Account findAccountById(Integer account); /** * 保存 * @param account */ void saveAccount(Account account); /** * 更新 * @param account */ void updateAccount(Account account); /** * 刪除 * @param id */ void deleteAccountById(Integer id); }
service 接口實現:
public class AccountServiceImpl implements IAccountService { private IAccountDao accountDao; /** * set注入 * @param accountDao */ public void setAccountDao(IAccountDao accountDao) { this.accountDao = accountDao; } /** * 獲取全部帳戶信息 * @return */ public List<Account> findAccounts() { return accountDao.findAccounts(); } public Account findAccountById(Integer account) { return accountDao.findAccountById(account); } public void saveAccount(Account account) { accountDao.saveAccount(account); } public void updateAccount(Account account) { accountDao.updateAccount(account); } public void deleteAccountById(Integer id) { accountDao.deleteAccountById(id); } }
Spring 上下文配置:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!--配置service--> <bean id="accountService" class="yh.service.impl.AccountServiceImpl"> <property name="accountDao" ref="accountDao"/> </bean> <!--配置dao--> <bean id="accountDao" class="yh.dao.impl.AccountDaoImpl"> <property name="runner" ref="dbutils"/> </bean> <!--配置dbutils 避免多線程干擾,設此bean設爲多例--> <bean id="dbutils" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"> <constructor-arg name="ds" ref="dateScore"/> </bean> <!--配置數據源--> <bean id="dateScore" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <!--鏈接數據庫的基本信息--> <property name="driverClass" value="com.mysql.jdbc.Driver"/> <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mybatis?SSl=true&useUnicode=true&characterEncoding=utf8"/> <property name="user" value="root"/> <property name="password" value="root"/> </bean> </beans>
測試代碼:
public class Mytest { @Test public void testFindAll(){ //獲取容器 ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); //獲取業務層對象 IAccountService service = context.getBean("accountService", IAccountService.class); //調用方法 List<Account> accounts = service.findAccounts(); for (Account account : accounts) { System.out.println(account.toString()); } } @Test public void testFindOne(){ ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); IAccountService service = context.getBean("accountService", IAccountService.class); Account account = service.findAccountById(2); System.out.println(account.toString()); } @Test public void testSave(){ ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); IAccountService service = context.getBean("accountService", IAccountService.class); service.saveAccount(new Account(5,"ddd",999)); } @Test public void testUpdate(){ ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); IAccountService service = context.getBean("accountService", IAccountService.class); service.updateAccount(new Account(2,"bbb2",999)); } @Test public void testDelete(){ ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); IAccountService service = context.getBean("accountService", IAccountService.class); service.deleteAccountById(2); } }
從 JDK 5.0 開始,java 增長了對元數據(MetaData)的支持,也就是 Annotation(註解)
註解是代碼裏的特殊標記,能夠在編譯、類加載、運行時被讀取,並執行相應的處理,經過使用註解,咱們能夠在不改變原有邏輯的狀況下,在源文件中嵌入一些補充信息。代碼分析工具、開發工具和部署工具能夠經過這些補充信息進行驗證或進行部署。
註解能夠像修飾符同樣被使用,用來修飾包、類、構造器、方法、成員變量、參數、局部變量的聲明,這些信息被保存在 Annation 的 name=value 對中。
框架 = 註解 + 反射 + 設計模式
自定義註解
使用 @interface 關鍵字如:public @interface testAnnotation
,自定義註解自動繼承了 java.lang.annotation.Annotation
接口;
註解的成員變量在定義時以無參方法的形式來聲明(如:String[] value()
),其方法名和返回值定義了該成員變量的名字和類型,此爲配置參數,類型只能是八種基本數據類型、String、Class、enum、Annotation 這幾個類型的數組(有多個 value 值);
能夠在定義註解的成員變量時使用 default 關鍵字,爲其指定初始值。若是隻有一個參數成員,建議設置參數名爲 value
若是定義的註解有配置參數,那麼使用時必須指定參數值,除非它有默認值。格式:參數名 = 參數值,若是隻有一個參數成員,且名稱爲 value,能夠省略 value = 參數值,直接寫參數值便可;
沒有成員定義的註解稱爲標記(如:@Override)包含成員變量的註解稱爲元數據註解
注意:自定義註解必須配上註解的信息處理流程(使用反射)纔有意義。
JDK 中的元註解
元註解:對現有註解進行解釋說明的註解。
jdk 提供的 4 中元註解:
@Retention:用於修飾一個 Annotation 定義,指定其生命週期,包含一個 RetentionPolicy 類型的成員變量,使用時需指定 value 的值:
RetentionPolicy.SOURCE:在源文件中有效(即源文件保留),編譯器直接丟棄這種策略的註釋;
RetentionPolicy.CLASS:在 class 文件中有效(即 class 保留),當運行 Java 程序時,JVM 不會保留註釋。這是默認值;
RetentionPolicy.RUNTIME:在運行時有效(即運行時保留),當運行 Java 程序時,JVM 會保留註釋。程序能夠經過反射獲取該註釋;
擴展:元數據,是指對數據進行修飾的數據。如:在
String name = "YunHe";
中"YunHe"
爲數據,而String name =
就爲元數據
配置註解與配置 xml 文件要實現的功能是同樣的,都是要下降程序間的耦合,只是配置的形式不同 。
與 xml 配置對應,可將註解簡單分爲:
用於建立對象的:
至關於:<bean id="" class=""> @Component: 做用:用於把當前類對象存入 spring 容器中 屬性: value:用於指定 bean 的 id。默認值爲當前類名首字母小寫 @Controller:通常用在表現層 @Service:通常用在業務層 @Repository:通常用在持久層 以上三個註解做用和屬性與 @Component 同樣(父子關係),是 spring 框架提供明確的三層使用註解, 使咱們的三層對象更加清晰
用於注入數據的:
至關於:<property name="" ref=""> / <property name="" value=""> @Autowired 做用:自動按照類型注入(自動裝配)。當使用註解注入屬性時,set方法能夠省略。自動將spring容器中 的 bean 注入到類型匹配的帶有此註解的屬性。當有多個類型匹配時,配合@Qualifier指定要注入的bean @Qualifier 做用:在自動按照類型注入的基礎之上,再按照bean的id注入(解決自動注入存在多個同類型的 bean 所產生的歧義問題),它在給字段注入時不能獨立使用,必須和 @Autowire 一塊兒使用;可是給方法參數注 入時,能夠獨立使用(指定形參所要接收的bean的id名)。 屬性: value:指定bean的id @Resource 做用:直接按照bean的id注入,它也只能注入其餘bean類型 屬性: name:指定bean的id @Value 做用:注入基本數據類型和 String 類型數據 屬性: value:用於指定值
用於改變做用範圍的:
至關於:<bean id="" class="" scope=""> @Scope 做用:指定 bean 的做用範圍 屬性: value:指定範圍的值 取值:singleton/prototype/request/session/globalsession
聲明週期相關:
至關於:<bean id="" class="" init-method="" destroy-method=""> @PostConstruct 做用:用於指定初始化方法 @PreDestroy 做用:用於指定銷燬方法
自動按照類型注入示意圖:
注意:spring 識別 bean 的範圍時需經過 xml 配置設置 spring 建立容器時要掃描的包。
這裏就貼上修改的部分代碼(改動過小了)
dao 實現類:(兩個註解)
@Repository("accountDao") public class AccountDaoImpl implements IAccountDao { @Autowired private QueryRunner runner; //... }
service 實現類:(兩個註解)
@Service("accountService") public class AccountServiceImpl implements IAccountService { @Autowired private IAccountDao accountDao; //... }
使用註解的 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:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!-- 告知 spring 建立容器時要掃描的包--> <context:component-scan base-package="yh"/> <bean id="dbutils" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"> <constructor-arg name="ds" ref="dateScore"/> </bean> <bean id="dateScore" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="driverClass" value="com.mysql.jdbc.Driver"/> <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mybatis?SSl=true&useUnicode=true&characterEncoding=utf8"/> <property name="user" value="root"/> <property name="password" value="root"/> </bean> </beans>
這樣就完成配置了,可是 DBUtils 及 c3p0 的配置能不能也轉換成註解形式呢?答案是固然能夠,這就要新引入一個配置類的概念。
在項目中新建一個結構以下:
並新建配置類以下:
/** * 該類是一個配置類它的做用和bean.xml是同樣的 * spring中的新註解: * * Configuration 註解 * 做用:指定當前類是一個配置類 * ComponentScan 註解 * 做用:指定Spring在建立容器時掃描配置的包 * 屬性:value,指定要包 * 效果同xml配置中的<context:component-scan base-package=""/>同樣 * Bean 註解 * 做用:用於把當前方法的返回值做爲bean對象存入spring的IoC容器中 * 屬性: * name:用於指定bean的id,當不寫時,默認爲方法名 * 細節: * 當咱們使用註解配置方法時,若是方法有參數,spring框架會去容器中查找有沒有可用的bean對象 * 查找的方式和Autowired註解的做用同樣 * Import 註解 * 做用:用於導入其餘配置的類 * 屬性: * value:用於指定其餘配置類的字節碼 * 當咱們使用Import的註解以後,使用Import註解的類就是父配置類,而導入的都是子配置類 * @author YH * @create 2020-05-22 21:36 */ @Configuration @ComponentScan("yh") @Import(JDBCConfig.class) public class SpringConfiguration { /** * 用於建立一個QueryRunner對象 * 細節:默認獲取的是單例的,但runner對象咱們須要多例的,因此可加上scope * @param dataSource * @return */ @Bean(name="runner") @Scope("prototype") public QueryRunner createQueryRunner(DataSource dataSource){ return new QueryRunner(dataSource); } }
配置jdbc的配置類:
/** * 註解方式獲取jdbc鏈接的配置類 * PropertySource 註解 * 做用:指定properties文件的位置 * 屬性: * value 註解:指定文件的名稱和路徑(properties文件的key) * 關鍵字:classpath,即是類路徑下 * @author YH * @create 2020-05-23 9:47 */ @PropertySource("classpath:data.properties") //引入外部的properties屬性文件 public class JDBCConfig { @Value("${jdbc.driver}") private String driver; @Value("${jdbc.url}") private String url; @Value("${jdbc.username}") private String username; @Value("${jdbc.password}") private String password; /** * 建立數據源 * @return */ @Bean(name="dataSource") public DataSource createDataSource(){ try { ComboPooledDataSource ds = new ComboPooledDataSource(); ds.setDriverClass(driver); ds.setJdbcUrl(url); ds.setUser(username); ds.setPassword(password); return ds; } catch (Exception e) { throw new RuntimeException(e); } } }
properties 配置的數據庫參數:
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/mybatis?useSSL=true&useUnicode=true&characterEncoding=utf8 #不要用username做爲key(獲取到了我程序的做者標記了) jdbc.username=root jdbc.password=root
如上就是純註解的配置形式,配置類的做用同 bean.xml
同樣,因此相對的,也會有對應 xml 中配置的註解(每每能見名知意)。測試類原來是經過加載 xml 的方式也要變動爲 Annotation 的,以下:
@Test public void testFindAll(){ // ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); //改成註解工廠 ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfiguration.class); IAccountService service = context.getBean("accountService", IAccountService.class); List<Account> accounts = service.findAccounts(); for (Account account : accounts) { System.out.println(account.toString()); } }
純註解的形式配置的工做量也不小,因此合理與 xml 搭配使用方能體現效率。
可使用在類或屬性上以及方法形參前,用於解決有多個同類型 bean 的自動注入問題,經過 @Qualifier()
指定 bean id 來確認哪一個 bean 纔是咱們須要注入的(設置的 value 值須要與目標 bean id 名相同)
@Primary
註解也用於解決自動注入時多個相同類型 bean 的問題,它定義了首選項,除非另有說明,不然將優先使用與@Primary
關聯的 bean。
在上面的測試代碼中都會有如下兩行代碼:
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); IAccountService service = context.getBean("accountService", IAccountService.class);
這兩行代碼的做用是獲取容器,若是不寫會提示空指針,因此不能輕易去掉。
針對問題,咱們須要 Spring 自動幫咱們建立容器,咱們就無序手動建立了,上面的問題也能解決。
首先 Junit 實現底層是集成了 main 方法,它沒法知曉咱們是否使用了 Spring 框架,天然無可能幫咱們建立容器,不過 junit 給我嗎暴露了一個註解,可讓咱們替換掉它的運行器。
因此咱們須要依賴 spring 框架,由於它提供了一個運行器,能夠讀取配置文件(或註解)來建立容器。咱們只須要告訴它配置文件的位置便可。
添加 junit 必備的 jar 包依賴
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.2.6.RELEASE</version> </dependency>
對於 Spring 5,需用 4.12 及以上 Junit jar 包
使用 @RunWith
註解替換原有運行器並使用@Autowired
給測試類中的變量注入數據
/** * RunWith:替換原有運行器 * @author YH * @create 2020-05-22 21:20 */ @RunWith(SpringJUnit4ClassRunner.class) public class MyTest { //由spring自動注入業務層對象 @Autowired IAccountService service; @Test public void testFindAll(){ List<Account> accounts = service.findAccounts(); for (Account account : accounts) { System.out.println(account.toString()); } } }
使用 @ContextConfiguration
指定 Spring 配置文件的位置
/** * RunWith:替換原有運行器 * ContextConfiguration * 屬性: * location屬性:用於指定配置文件的位置,若是是類路徑下,須要用classpath:表名 * classes屬性:用於指定註解的類,當不使用xml配置時,須要用此屬性指定註解類的位置 * @author YH * @create 2020-05-22 21:20 */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes={SpringConfiguration.class}) //{}表示支持設置多個配置類 public class MyTest { @Autowired ApplicationContext context; @Autowired IAccountService service; @Test public void testFindAll(){ List<Account> accounts = service.findAccounts(); for (Account account : accounts) { System.out.println(account.toString()); } } }
其中@Autowired
會給測試類中的變量注入數據
首先,測試類配置到 xml 中確定是能夠實現的,但爲何不這樣作?
緣由:
當咱們在 xml 中配置一個 bean ,spring 加載配置文件建立容器時,就會建立對象。
而測試僅僅起測試做用,在項目中它並不參與程序邏輯,也不會解決需求上的問題,因此建立完了,並無使用,那麼存在容器中就會形成資源的浪費。
因此,基於以上兩點,咱們不該該把測試類配置到 xml 中。
AOP(Aspect Oriented Programming)面向切面編程,經過預編譯的方式和運行期動態代理實現程序功能的統一維護的一種技術。將程序中重複的功能代碼抽象出來,在須要執行的時候使用動態代理在不修改源碼的基礎上,對咱們已有的方法進行加強。
從幾個知識面做爲學習 AOP 的突破口
修改上面的 CRUD 案例,首先原案例代碼中的事務由 connection 對象的 setAutocommit(true)
而被自動控制。此方式控制事務,一次只執行一條 sql 語句,沒有問題,但執行多條 sql 就沒法實現功能。緣由是 sql 執行一次會獲取一次數據庫鏈接,統一 sql 語句的執行結果會被緩存,後面執行會直接讀取緩存;而多條 sql 執行就須要各自或許鏈接並執行,持久層方法都是獨立事務的,不符合事務的一致性,下面來探討一下。
持久層代碼:
package yh.dao.impl; import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.handlers.BeanListHandler; import yh.dao.IAccountDao; import yh.pojo.Account; import java.sql.SQLException; import java.util.List; /** * @author YH * @create 2020-07-01 8:59 */ public class AccountDaoImpl implements IAccountDao { private QueryRunner runner; public void setRunner(QueryRunner runner){ this.runner = runner; } @Override public Account findName(String name) { try { List<Account> accounts = runner.query("select * from mybatis.account where name=?", new BeanListHandler<Account>(Account.class), name); if(accounts == null || accounts.isEmpty()){ return null; } if (accounts.size() > 1){ throw new RuntimeException("結果集不惟一,數據有問題"); } return accounts.get(0); } catch (Exception e) { e.printStackTrace(); } return null; } @Override public void update(Account account) { try { runner.update("update mybatis.account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId()); } catch (SQLException e) { e.printStackTrace(); } } }
業務層代碼:
package yh.service.impl; import yh.dao.impl.AccountDaoImpl; import yh.pojo.Account; import yh.service.IAccountService; /** * @author YH * @create 2020-07-01 8:53 */ public class AccountServiceImpl implements IAccountService { private AccountDaoImpl accountDao; public void setAccountDao(AccountDaoImpl accountDao){ this.accountDao = accountDao; } @Override public void transfer(String sourceName, String targetName, Float money) { //根據帳戶信息獲取帳戶對象 Account source = accountDao.findName(sourceName); Account target = accountDao.findName(targetName); //轉出帳戶減錢,轉入帳戶加錢 source.setMoney(source.getMoney() - money); target.setMoney(target.getMoney() + money); //提交更新 accountDao.update(source); int i = 1/0;//模擬程序出錯 accountDao.update(target); } }
理想狀況下,程序正常運行,轉帳結果正確
一旦出錯,前面執行後面的執行中斷,即轉出帳戶減錢了,而收款帳戶餘額未增長,且事務沒法回滾(由於它們有各自的事務)
下面就是新增在業務層的轉帳方法,每一個執行方法都獲取一次鏈接,都是獨立的事務,一旦中途出現中斷,就沒法實現事務的回滾。
解決辦法:
使用 ThreadLocal 對象把 Connection 和當前線程綁定,從而使一個線程中只有一個能控制事務的對象,原來的事務是在持久層,現需將事務應用在業務層。
持久層代碼:
public class AccountDaoImpl implements IAccountDao { private QueryRunner runner; private ConnectionUtils connectionUtils; public void setRunner(QueryRunner runner){ this.runner = runner; } public void setConnectionUtils (ConnectionUtils connectionUtils){ this.connectionUtils = connectionUtils; } @Override public Account findName(String name) { try { //使用與線程綁定的鏈接 List<Account> accounts = runner.query(connectionUtils.getThreadConnection(),"select * from mybatis.account where name=?", new BeanListHandler<Account>(Account.class), name); if(accounts == null || accounts.isEmpty()){ return null; } if (accounts.size() > 1){ throw new RuntimeException("結果集不惟一,數據有問題"); } return accounts.get(0); } catch (Exception e) { e.printStackTrace(); } return null; } @Override public void update(Account account) { try { //使用與線程綁定的鏈接 runner.update(connectionUtils.getThreadConnection(),"update mybatis.account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId()); } catch (SQLException e) { e.printStackTrace(); } } }
業務層代碼:
public class AccountServiceImpl implements IAccountService { private AccountDaoImpl accountDao; private TransactionManager transactionManager; public void setAccountDao(AccountDaoImpl accountDao){ this.accountDao = accountDao; } public void setTransactionManager(TransactionManager transactionManager){ this.transactionManager = transactionManager; } @Override public void transfer(String sourceName, String targetName, Float money) { //根據帳戶信息獲取帳戶對象 Account source = accountDao.findName(sourceName); Account target = accountDao.findName(targetName); try {//開啓事務 transactionManager.beginTransaction(); //轉出帳戶減錢,轉入帳戶加錢 source.setMoney(source.getMoney() - money); target.setMoney(target.getMoney() + money); //提交更新 accountDao.update(source); // int i = 1 / 0; accountDao.update(target); //提交事務 transactionManager.commit(); } catch (Exception e){ transactionManager.rollback(); e.printStackTrace(); }finally { //釋放線程並解綁鏈接 transactionManager.release(); } } }
鏈接工具類代碼:
package yh.utils; import javax.sql.DataSource; import java.sql.Connection; /** * 鏈接的工具類 * 從數據源中獲取鏈接,並實現和線程的綁定 * @author YH * @create 2020-07-02 9:30 */ public class ConnectionUtils { private ThreadLocal<Connection> tl = new ThreadLocal<>(); private DataSource dataSource; public void setDataSource(DataSource dataSource){ this.dataSource = dataSource; } /** * 獲取當前線程上的鏈接 * @return */ public Connection getThreadConnection(){ try { //1.先從Threadlocal上獲取鏈接 Connection conn = tl.get(); //2.判斷當前線程上是否有鏈接 if (conn == null) { //3.若是ThreadLocal上沒有鏈接,那麼從數據源獲取鏈接並存入ThreadLocal conn = dataSource.getConnection(); tl.set(conn); } //4.返回當前線程鏈接 return conn; } catch(Exception e){ throw new RuntimeException(e); } } /** * 直接刪除鏈接,讓線程與鏈接解綁 */ public void removeConnection(){ tl.remove(); } }
事務管理工具類的代碼:
package yh.utils; import java.sql.SQLException; /** * 事務管理相關的工具類 * 負責開啓事務、提交事務、回滾事務、釋放鏈接 * @author YH * @create 2020-07-02 9:57 */ public class TransactionManager { private ConnectionUtils connectionUtils; public void setConnectionUtils(ConnectionUtils connectionUtils){ this.connectionUtils = connectionUtils; } /** * 開啓事務 */ public void beginTransaction(){ try { connectionUtils.getThreadConnection().setAutoCommit(false); } catch (Exception e) { e.printStackTrace(); } } /** * 提交事務 */ public void commit(){ try { connectionUtils.getThreadConnection().commit(); } catch (Exception e) { e.printStackTrace(); } } /** * 回滾事務 */ public void rollback(){ try { connectionUtils.getThreadConnection().rollback(); } catch (Exception e) { e.printStackTrace(); } } /** * 釋放資源 並 解綁線程和鏈接 * 默認狀況下線程回收到線程池其上依舊綁定了已經會受到鏈接池的鏈接, * 即鏈接時關閉的,再次啓動線程時,能直接獲取到鏈接,但這個鏈接顯然 * 沒法使用,顧需在線程關閉後讓其與鏈接解綁 */ public void release(){ try { //回收到線程池 connectionUtils.getThreadConnection().close(); connectionUtils.removeConnection(); } catch (Exception e) { e.printStackTrace(); } } }
線程回收到線程池,而線程綁定的鏈接也會回到鏈接池,若是該線程在此運行,那麼此時獲取它的鏈接是能夠獲取到的,但這個鏈接已經關閉回到鏈接池中,這樣顯然不行。因此在線程關閉前還須要作線程解綁操做。
解決事務問題後,發現我只是增長一個功能,就要對原有代碼進行這麼大的改動,並且業務層和持久層對兩個工具類方法有很強的依賴,顯然這就是問題,有什麼解決辦法呢?
場景:有生產者(被代理類)與經銷商(代理方)。生產者能夠售出產品,經銷商也能夠銷售產品,但由經銷商銷售的產品經銷商從中收取百分之20的金額。即要對被代理的工廠增長代理的代碼,使得代理經銷商能收益。以下:
基於接口的代理
共同實現的接口:
/** * 定義一個代理類和被代理類共同要實現的接口 * 從而實現基於接口的代理 * @author YH * @create 2020-07-02 14:37 */ public interface IProducer { /** * 銷售產品 * @param money */ public void saleProduct(float money); /** * 產品售後 * @param money */ public void afterProduct(float money); }
生產者(被代理對象):
public class Producer implements IProducer { public void saleProduct(float money){ System.out.println("銷售產品,並拿到錢:" + money); } public void afterProduct(float money){ System.out.println("產品售後,並拿到錢:" + money); } }
模擬消費(代理對象):
public class Client { public static void main(String[] args){ //被代理對象(被內部類方法,須要聲明爲不可變的) final Producer producer = new Producer(); /** * 動態代理: * 特色:字節碼隨意調用,隨用隨加載 * 做用:不修改源碼的基礎上對方法加強 * 分類: * 基於接口的動態代理 * 基於子類的動態代理 * 基於接口的動態代理: * 涉及的類:Proxy * 提供者:官方JDK * 如何建立代理對象: * 使用Proxy類的newProxyInstance方法 * 建立代理對象的要求: * 被代理類至少實現一個接口,若是沒有則不能使用 * newProxyInstance方法的參數: * lassLoader:類加載器。用於加載代理對象字節碼的,和被代理對象使用相同的類加載器。固定寫法 * Class<?>[]:字節碼數組。傳遞被代理對象實現的接口信息,使得代理對象和被代理對象具備相同的方法。固定寫法 * InvocationHandler:用於提供加強的代碼。用於說明如何代理(通常寫一些接口的實現類,一般是匿名內部類) */ IProducer proxyProducer = (IProducer)Proxy.newProxyInstance(producer.getClass().getClassLoader(), producer.getClass().getInterfaces(), new InvocationHandler(){ /** * 執行被代理對象的任何方法都會通過這裏 * @param o 代理對象的引用 * @param method 當前執行的方法 * @param objects 當前執行方法所需的參數 * @return 和被代理對象有相同的返回值 * @throws Throwable */ public Object invoke(Object o, Method method, Object[] objects) throws Throwable { //提供加強的代碼 //1.獲取方法執行的參數 Float money = (Float)objects[0]; //2.判斷當前方法是否是銷售 if("saleProduct".equals(method.getName())) { return method.invoke(producer, money * 0.8f); } return null; } }); //測試調用被代理類的方法 proxyProducer.saleProduct(10000f); } }
最終實現了,在經銷商處銷售的商品工廠只能拿到8000。
基於接口的代理方式有一個缺陷就是必需要實現一個接口,沒法實現接口要怎麼辦呢,那就是實現動態代理的另外一種方式:基於子類的動態代理
這種方式須要有第三方 jar 包: cglib
的支持
增長 pom.xml 文件依賴:
<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.3.0</version> </dependency>
模擬消費(代理類)
/** * 基於子類的動態代理 * @author YH * @create 2020-07-02 16:53 */ public class Client { public static void main(String[] args){ final Producer producer = new Producer(); /** * 基於接口的動態代理: * 涉及的類:Enhancer * 提供者:第三方cglib * 如何建立代理對象: * 使用Enhancer類的create()方法 * 建立代理對象的要求: * 被代理類不能是最終類 * create()方法的參數: * class:字節碼。用於指定被代理對象的字節碼 * Callback:用於提供加強的代碼。即如何代理,通常用該接口的子類接口的實現類 MethodInterceptor */ Producer cglibProduct = (Producer ) Enhancer.create(producer.getClass(), new MethodInterceptor() { public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { //提供加強的代碼 //1.獲取方法執行的參數 Float money = (Float)objects[0]; //2.判斷當前方法是否是銷售 if("saleProduct".equals(method.getName())) { return method.invoke(producer, money * 0.8f); } return null; } }); //測試調用方法 cglibProduct.saleProduct(10000f); } }
生產者(被代理類)
public class Producer implements IProducer { public void saleProduct(float money){ System.out.println("銷售產品,並拿到錢:" + money); } public void afterProduct(float money){ System.out.println("產品售後,並拿到錢:" + money); } }
結果相同:
持久層代碼不變
業務層代碼(被代理對象):
public class AccountServiceImpl implements IAccountService { private AccountDaoImpl accountDao; public void setAccountDao(AccountDaoImpl accountDao){ this.accountDao = accountDao; } @Override public void transfer(String sourceName, String targetName, Float money) { try { //根據帳戶信息獲取帳戶對象 Account source = accountDao.findName(sourceName); Account target = accountDao.findName(targetName); //轉出帳戶減錢,轉入帳戶加錢 source.setMoney(source.getMoney() - money); target.setMoney(target.getMoney() + money); //提交更新 accountDao.update(source); // int i = 1 / 0; accountDao.update(target); } catch (Exception e){ //改成運行時異常,將異常拋給調用者(代理類)來處理,不然調用處後的回滾操做沒法執行 // (固然被代理類中也能夠不捕獲異常,代理類捕獲) throw new RuntimeException(e); } } }
代理工廠:
package yh.factory; import yh.service.IAccountService; import yh.utils.TransactionManager; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; /** * 用於建立Service的代理對象工廠 * @author YH * @create 2020-07-03 8:55 */ public class BeanFactory { private IAccountService accountService; private TransactionManager transactionManager; public void setAccountService(IAccountService accountService){ this.accountService = accountService; } public void setTransactionManager(TransactionManager transactionManager){ this.transactionManager = transactionManager; } public IAccountService getAccountService(){ return (IAccountService)Proxy.newProxyInstance(accountService.getClass().getClassLoader(), accountService.getClass().getInterfaces(), new InvocationHandler() { /** * 獲取AccountService的代理對象 * @param proxy * @param method * @param args * @return * @throws Throwable */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object returnValue = null; try { //1.開啓事務 transactionManager.beginTransaction(); //2.執行操做 returnValue = method.invoke(accountService, args); //3.提交事務 transactionManager.commit(); //4.返回被代理對象 return returnValue; } catch(Exception e){ //5.回滾 transactionManager.rollback(); throw new RuntimeException(e); } finally { //6.釋放資源 transactionManager.release(); } } }); } }
被代理對象實現了一個接口,顧使用了基於接口的動態代理方式。
至此,不管業務層中有多少個方法,都會由代理類爲其增長事務管理,而不是每一個單獨都要設置,在不增長業務類代碼的狀況下實現了功能的加強!
使用 AOP 就能夠經過配置的方式實現上面案例的功能,這也是經過案例引入 AOP 的緣由。
AOP 相關術語
Joinpoint(鏈接點):
指那些被攔截到的點。在 Spring 中,這些點指的是方法,由於 Spring 只支持方法類型的鏈接點
Pointcut(切點):
切點的定義會匹配通知所要織入的一個或多個鏈接點,即定義攔截規則(一般使用明確的類和方法名稱,可配合正則表達式使用)
Advice(通知/加強):
攔截到 Joinpoint 以後要作的事情(新增的功能)
通知的類型:前置通知、後置通知、異常通知、最終通知、環繞通知。對應到案例中以下:
Introduction(引入):
一種特殊的通知。在不修改類代碼的前提下,Introduction 能夠在運行期爲類動態地添加一些方法或屬性
Target(目標對象):
代理的目標對象
Weaving(織入):
把加強應用到目標對象並建立新的代理對象的過程。
Spring 採用動態代理織入(運行期);AspectJ 採用編譯器織入和類裝載期織入。
Proxy(代理):
一個類被 AOP 織入加強後,就會產生一個結果代理類
Aspect(切面):
切點和通知的結合
小結:
通知包含了須要應用於多個對象的橫切行爲;鏈接點是程序執行過程當中可以應用通知的全部點;切點定義了通知被應用的具體位置,即哪些鏈接點(方法),且定義了哪些鏈接點會獲得通知。
注意
開發階段(咱們作的)
運行階段(Spring 框架作的)
Spring 會根據目標類是否實現了接口來決定採用哪一種動態代理方式。
動態代理中用到的
invoke()
方法有攔截功能。
添加依賴
<!--用於解析Spring表達式--> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.5</version> </dependency>
bean.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:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- ioc配置,將Service配置進來--> <bean id="accountService" class="yh.service.impl.AccountServiceImpl"/> <!-- aop配置 1.把通知bean也交給spring管理 2.使用aop:config標籤標示開始aop配置 3.使用aop:aspect變遷配置切面 id:給切面定義惟一的表示 ref:指定切面的通知類bean的id 4.內部標籤中配置通知類型(前置通知爲例) 使用aop:before表示配置前置通知 method:指定通知列中哪一個方法用於通知 pointcut:指定切入點表達式,表示對業務層中哪些方法進行加強 切入點表達式寫法: 關鍵字:execution(表達式) 標準寫法 execution(public void 全類名.方法名(參數列表)) 其中權限修飾符能夠省略,返回值類型、全類名、方法名、形參列表均可以用通配符代替 全統配寫法:* *..*.*(..) 多個包用 .. 表示一個包及其子包,形參列表.. 表示無參或多參 但實際開發中只會對業務層的實現類方法進行統配,寫法:* 業務層包路徑.*.*(..) --> <!-- 配置Logger類--> <bean id="logger" class="yh.utils.Logger"/> <!--配置aop--> <aop:config> <!--配置切面--> <aop:aspect id="loggerAdvice" ref="logger"> <!--配置通知類型且定義通知方法和切入點方法的關聯--> <aop:before method="printLog" pointcut="execution(public void yh.service.impl.AccountServiceImpl.saveAccount())"/> </aop:aspect> </aop:config> </beans>
注意添加 aop 命名空間和約束
業務層接口
public interface IAccountService { /** * 模擬保存帳戶 */ void saveAccount(); /** * 模式更新帳戶 * @param i */ void updateAccount(int i); /** * 模擬刪除帳戶 * @return */ int deleteAccount(); }
業務層實現類
public class AccountServiceImpl implements IAccountService { public void saveAccount() { System.out.println("save account!"); } public void updateAccount(int i) { System.out.println("update account!"); } public int deleteAccount() { System.out.println("delete account!"); return 1; } }
通知類
public class Logger { /** * 輸出日誌:計劃讓其在切入點以前執行(即前置通知,在匹配的業務層方法前執行) */ public void printLog(){ System.out.println("輸出日誌..."); } }
測試
@Test public void aopTest(){ //1.獲取容器 ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml"); //2.獲取bean IAccountService as = (IAccountService)context.getBean("accountService"); //3.執行方法 as.saveAccount(); //spring表達式所匹配的鏈接點方法纔會被應用通知 as.updateAccount(1); as.deleteAccount(); }
結果:
增長對應的方法,對四種通知進行配置:
<aop:config> <!--提取共用的表達式,供通知引用--> <aop:pointcut id="ref" expression="execution(* yh.service.impl.*.*(..))"/> <!--配置切面--> <aop:aspect id="loggerAdvice" ref="logger"> <!--前置通知:切入點方法執行前通知--> <aop:before method="beforeAdvice" pointcut-ref="ref"/> <!--後置通知:切入點方法執行後通知--> <aop:after-returning method="afterAdvice" pointcut-ref="ref"/> <!--異常通知:切入點方法拋出異常時通知--> <aop:after-throwing method="exceptionAdvice" pointcut-ref="ref"/> <!--最終通知:不管切入點方法是否正常執行,都會執行--> <aop:after method="finallyAdvice" pointcut-ref="ref"/> </aop:aspect> </aop:config>
使用所編寫的邏輯將被通知的目標方法徹底包裝起來(相似前面的動態代理對方法的加強),實現了一個方法中同時編寫各種通知。
bean.xml中配置環繞通知
<!--配置環繞通知--> <aop:around method="aroundAdvice" pointcut-ref="ref"/>
通知類中定義環繞通知的方法:
/** * 環繞通知 * Spring提供了一個接口:ProceedingJoinPoint,改接口有一個 proceed() 方法,用於明確切入點方法 * 改接口可做爲環繞通知方法的參數使用,由Spring建立 * 經過環繞通知咱們能夠手動控制被加強方法在通知中執行的位置 */ public Object aroundAdvice(ProceedingJoinPoint pjp){ Object returnValue = null; try { System.out.println("我是前置通知"); //獲得執行方法所需的參數 Object[] args = pjp.getArgs(); //執行切入點(業務類)方法 returnValue = pjp.proceed(args); System.out.println("我是後置通知"); } catch (Throwable throwable) { System.out.println("我是異常通知"); throwable.printStackTrace(); } finally { System.out.println("我是最終通知"); } return returnValue; }
相似代理類環繞加強被代理類,但明顯更加簡便明瞭,大多數事情被 spring 完成了,咱們能夠在被通知方法執行先後定義想要增長的功能,從而實現各種通知,結果以下:
業務類要加上 @Service("accountService")
讓 Spring 容器管理並指定標識 id
通知類
/** * 記錄日誌的工具類,定義通知的共用代碼 * @author YH * @create 2020-07-04 7:27 * Component註解,指示Spring容器將建立管理當前類對象 * value:用於指定 bean 的 id。默認值爲當前類名首字母小寫 * (三層有各自的註解,但功能同樣,是Component的子類) * Aspect註解:表示當前類是一個切面 */ @Component("logger") @Aspect public class Logger { /** * 經過註解定義可重用切點表達式,供通註解知引用 */ @Pointcut("execution(* yh.service.impl.*.*(..))") public void spe(){} /** * 前置通知 */ @Before("spe()") public void beforeAdvice(){ System.out.println("前置通知..."); } /** * 後置通知 */ @AfterReturning("spe()") public void afterAdvice(){ System.out.println("後置通知..."); } /** * 異常通知 */ @AfterThrowing("spe()") public void exceptionAdvice(){ System.out.println("異常通知..."); } /** * 最終通知 */ @After("spe()") public void finallyAdvice(){ System.out.println("最終通知..."); } /** * 環繞通知 * Spring提供了一個接口:ProceedingJoinPoint,改接口有一個 proceed() 方法,用於明確切入點方法 * 改接口可做爲環繞通知方法的參數使用,由Spring建立 * 經過環繞通知咱們能夠手動控制被加強方法在通知中執行的位置 */ @Around("spe()") public Object aroundAdvice(ProceedingJoinPoint pjp){ Object returnValue = null; try { System.out.println("我是前置通知"); //獲得執行方法所需的參數 Object[] args = pjp.getArgs(); //執行切入點(業務類)方法 returnValue = pjp.proceed(args); System.out.println("我是後置通知"); } catch (Throwable throwable) { System.out.println("我是異常通知"); throwable.printStackTrace(); } finally { System.out.println("我是最終通知"); } return returnValue; } }
bean.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:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!-- 配置Spring建立容器時要掃描的包--> <context:component-scan base-package="yh"/> <!-- 開啓註解aop的支持--> <aop:aspectj-autoproxy/> </beans>
使用註解,命名空間和約束都須要設置
純註解獲取 Spring 容器方式與經過 xml 配合不同,以下:
先定義一個 java 配置類:
@Configuration @ComponentScan("yh") //指定掃描的包 @EnableAspectJAutoProxy //開啓基於註解AOP的支持 public class SpringConfiguration { }
/** * 測試純註解配置 */ @Test public void annotationAopTest2(){ ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfiguration.class); IAccountService service = (IAccountService)context.getBean("accountService"); service.saveAccount(); }
基於註解配置通知時,建議應用於環繞通知。其餘通知的順序可能不是想要的結果(如後置通知在最終通知以前執行)
基於 XML 配置
改動幾乎都在 bean.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:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <!--配置service--> <bean id="accountService" class="yh.service.impl.AccountServiceImpl"> <property name="accountDao" ref="accountDao"/> </bean> <!--配置dao--> <bean id="accountDao" class="yh.dao.impl.AccountDaoImpl"> <property name="runner" ref="runner"/> <property name="connectionUtils" ref="connectionUtils"/> </bean> <!-- 配置QueryRunner--> <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"> <constructor-arg ref="dataSource"/> </bean> <!--配置數據源--> <!-- 讀取數據源文件的位置--> <context:property-placeholder location="classpath:jdbc.properties"/> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${driverClassName}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <!-- 配置事務管理工具類--> <bean id="transactionManager" class="yh.utils.TransactionManager"> <property name="connectionUtils" ref="connectionUtils"/> </bean> <!-- 配置鏈接工具類--> <bean id="connectionUtils" class="yh.utils.ConnectionUtils"> <property name="dataSource" ref="dataSource"/> </bean> <!-- AOP配置--> <aop:config> <!--提取共用的表達式,供通知引用--> <aop:pointcut id="pe1" expression="execution(* yh.service.impl.AccountServiceImpl.transfer(..))"/> <!--配置切面--> <aop:aspect id="tr" ref="transactionManager"> <!--肯定通知類型,定義通知方法和切入點方法的關聯--> <aop:before method="beginTransaction" pointcut-ref="pe1"/> <aop:after-throwing method="rollback" pointcut-ref="pe1"/> <aop:after-returning method="commit" pointcut-ref="pe1"/> <aop:after method="release" pointcut-ref="pe1"/> </aop:aspect> </aop:config> </beans>
純註解配置
基於註解配置中,因爲 Spring 緣由,最終通知(@After)和後置通知(@AfterReturning)或異常通知(@AfterThrowing)的執行順序沒法控制,因此使用環繞通知:
持久層、業務層等工具列類只用加上組件註解(@Component 註解之類)以及其成員屬性的注入註解(@Autowired 註解)便可
SpringConfiguration 配置類:
@Configuration //表名此類爲配置類 @EnableAspectJAutoProxy //開啓Spring AOP支持 @ComponentScan("yh") //指定spring建立容器要掃描的包 @Import(JdbcConfig.class) //導入子配置類 public class SpringConfiguration { @Bean(name = "runner") //將方法的返回值建立爲bean 並存入Spring容器中 public QueryRunner createQueryRunner(DataSource dataSource){//形參可自動注入 return new QueryRunner(dataSource); } }
JDBC配置類:
@PropertySource("classpath:jdbc.properties") //引入外部properties屬性文件 public class JdbcConfig { //@Value是@PropertySource的屬性註解,用於讀取配置文件中的key-value @Value("${driverClassName}") private String driver; @Value("${jdbc.url}") private String url; @Value("${jdbc.username}") private String username; @Value("${jdbc.password}") private String password; @Bean(name="dataSource") public DataSource createDataSource(){ DruidDataSource ds = new DruidDataSource(); ds.setDriverClassName(driver); ds.setUrl(url); ds.setUsername(username); ds.setPassword(password); return ds; } }
通知類:
/** * 事務管理相關的工具類 * 負責開啓事務、提交事務、回滾事務、釋放鏈接 * @author YH * @create 2020-07-02 9:57 */ @Component("txManager") @Aspect //指示此類是切面 public class TransactionManager { @Autowired private ConnectionUtils connectionUtils; @Pointcut("execution(* yh.service.impl.*.*(..))") public void spe(){} /** * 開啓事務 */ public void beginTransaction(){ try { connectionUtils.getThreadConnection().setAutoCommit(false); System.out.println("開啓事務"); } catch (Exception e) { e.printStackTrace(); } } /** * 提交事務 */ public void commit(){ try { connectionUtils.getThreadConnection().commit(); System.out.println("提交事務"); } catch (Exception e) { e.printStackTrace(); } } /** * 回滾事務 */ public void rollback(){ try { connectionUtils.getThreadConnection().rollback(); System.out.println("回滾事務"); } catch (Exception e) { e.printStackTrace(); } } /** * 釋放資源 並 解綁線程和鏈接 * 默認狀況下線程回收到線程池其上依舊綁定了已經會受到鏈接池的鏈接, * 即鏈接時關閉的,再次啓動線程時,能直接獲取到鏈接,但這個鏈接顯然 * 沒法使用,顧需在線程關閉後讓其與鏈接解綁 */ public void release(){ try { //回收到線程池 connectionUtils.getThreadConnection().close(); connectionUtils.removeConnection(); System.out.println("關閉資源"); } catch (Exception e) { e.printStackTrace(); } } /** * 環繞通知,配置註解通知建議只使用環繞通知 */ @Around("spe()") public Object aroundAdvice(ProceedingJoinPoint pjp){ Object returnValue = null; try { this.beginTransaction(); Object[] args = pjp.getArgs(); returnValue = pjp.proceed(args); this.commit(); } catch (Throwable throwable) { this.rollback(); throwable.printStackTrace(); } finally { this.release(); } return returnValue; } }
properties屬性文件:
driverClassName=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/mybatis?ssl=true&useUnicode=true&characterEncoding=utf8 jdbc.username=root jdbc.password=root
xml 引入外部屬性文件的兩種方式:
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="location" value="classpath:jdbc.properties"/> </bean><context:property-placeholder location="classpath:jdbc.properties"/>
Spring 框架提供了不少的操做模板類
關鍵依賴
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.2.6.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-tx --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.2.6.RELEASE</version> </dependency>
基本配置
<?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:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <!-- 配置JdbcTemplate--> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"/> </bean> <!-- 配置數據源--> <!--引入外部屬性文件--> <context:property-placeholder location="data.properties"/> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${driverClassName}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> </beans>
data.properties
driverClassName=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/mybatis?ssl=true&useUnicode=true&characterEncoding=utf8 jdbc.username=root jdbc.password=root
import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import yh.domain.Account; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; /** * JdbcTemplate的簡單用法 * @author YH * @create 2020-07-05 17:25 */ public class JdbcTemplate1 { public static void main(String[] args){ ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml"); JdbcTemplate jt = (JdbcTemplate)context.getBean("jdbcTemplate"); //保存 jt.update("insert into mybatis.account(name,money) values(?,?)","zzz",2000); //修改 jt.update("update mybatis.account set money=money+? where name=?",99,"aaa"); //刪除 jt.update("delete from mybatis.account where id=?",7); //查詢全部 List<Account> accountList = jt.query("select * from mybatis.account", new AccountRowMapper()); for(Account a : accountList){ System.out.println(a); } //查詢一個 List<Account> accountList = jt.query("select * from mybatis.account where id=?", new AccountRowMapper(),3); System.out.println(accountList.isEmpty() ? "沒有結果" : accountList.get(0)); //查詢返回一行一列,經常使用於分頁中獲取總記錄數 Integer total = jt.queryForObject("select count(*) from mybatis.account where id>?", Integer.class, 1); System.out.println(total); } /** * 處理查詢結果集的封裝 */ static class AccountRowMapper implements RowMapper<Account> { /** * @param resultSet 查詢sql返回的結果集 * @param i 所查詢表的行數 */ @Override public Account mapRow(ResultSet resultSet, int i) throws SQLException { Account account = new Account(); account.setId(resultSet.getInt("id")); account.setName(resultSet.getString("name")); account.setMoney(resultSet.getFloat("money")); return account; } }
dao 中使用 JdbcTemplate 有兩種方式,普通作法,在 dao 中增長一個 JdbcTemplate 引用屬性,交由 spring 注入,然後進行 update()、query() 調用。但當有多個 dao 時,每一個 dao 內都要重複定義代碼:private JdbcTemplate jdbcTemplate;
第二種方式:使用 Spring 提供的 JdbcDaoSupport 抽象類,其內部封裝了 JdbcTemplate 屬性,只需給予一個 DataSource 給它就能夠獲取 JdbcTemplate 對象,讓咱們的 dao 繼承它就能夠獲取屬性以及注入 DataSource:
、
持久層接口:
public interface IAccountDao { /** * 經過Id查帳戶 * @param id * @return */ public Account findAccountById(Integer id); /** * 經過Id查帳戶 * @param name * @return */ public Account findAccountByName(String name); /** * 修改帳戶 * @param account */ public void updateAccount(Account account); }
持久層實現類:
public class AccountDaoImpl extends JdbcDaoSupport implements IAccountDao { /*注:繼承父類所得到的屬性可進行注入,數據源就是經過此特性注入(見bean.xml)*/ @Override public Account findAccountById(Integer id) { JdbcTemplate jt = getJdbcTemplate(); List<Account> list = jt.query("select * from mybatis.account where id=?", new AccountRowMapper(), id); return list.isEmpty() ? null : list.get(0); } @Override public Account findAccountByName(String name) { JdbcTemplate jt = getJdbcTemplate(); List<Account> list = jt.query("select * from mybatis.account where name=?", new AccountRowMapper(), name); if(list.size() > 1){ throw new RuntimeException("結果集不惟一,查詢的對象有多個"); } return list.isEmpty() ? null : list.get(0); } @Override public void updateAccount(Account account) { JdbcTemplate jt = getJdbcTemplate(); jt.update("update mybatis.account set name=?,money=? where id=?", account.getName(),account.getMoney(),account.getId()); }
封裝查詢結果集的工具類:
public class AccountRowMapper implements RowMapper<Account> { @Override public Account mapRow(ResultSet resultSet, int i) throws SQLException { Account account = new Account(); account.setId(resultSet.getInt("id")); account.setName(resultSet.getString("name")); account.setMoney(resultSet.getFloat("money")); return account; } }
bean.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:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <!-- 配置dao--> <bean id="accountDao" class="yh.dao.impl.AccountDaoImpl"> <!--給所繼承的父類的屬性注入值--> <property name="dataSource" ref="dataSource"/> </bean> <!-- 配置數據源--> <!--引入外部屬性文件--> <context:property-placeholder location="data.properties"/> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${driverClassName}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> </beans>
data.properties
driverClassName=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/mybatis?ssl=true&useUnicode=true&characterEncoding=utf8 jdbc.username=root jdbc.password=root
注意:第一種方式可使用註解或者 xml 配置;但第二種方式只能用 xml 配置
JavaEE 體系進行分層開發,事務處理位於業務層,Spring 提供了分層設計業務層的事務處理解決方案。Spring 提供了一組基於 AOP 的事務控制接口 ,能夠經過編程或配置方式實現。
//獲取事務狀態信息 TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException; //提交事務 void commit(TransactionStatus var1) throws TransactionException; //回滾事務 void rollback(TransactionStatus var1) throws TransactionException;
開發中使用的是它額實現類對象對事務進行管理:
//使用 Spring JDBC 或 iBatis 進行持久化數據時使用 org.springframework.jdbc.datasource.DataSourceTransactionManager //使用 Hibernate 版本進行持久化數據時使用 org.springframework.orm.hibernate5.HibernateTransactionManager
//獲取事務對象的名稱 String getName(); //獲取事務隔離級別 int getIsolationLevel(); //獲取事務傳播行爲 int getPropagationBehavior(); //獲取事務超時時間 int getTimeout(); //獲取事務是否只讀 boolean isReadOnly();
讀寫型事務:增長、刪除、修改開啓事務;
只讀型事務:執行查詢時,也會開啓事務。
事務隔離級別
事務隔離級別反應了事務提交併發訪問時的處理態度
ISOLATION_DEFAULT
:默認級別,歸屬下列某一類ISOLATION_READ_UNCOMMITTED
:能夠讀取未提交數據ISOLATION_READ_COMMITTED
:只能讀取已提交數據,解決髒讀問題(Oracle 默認級別)ISOLATION_REPEATABLE_READ
:是否讀取其餘事務提交修改後的數據,解決不可重複讀取問題(MySQL默認級別)ISOLATION_SERIALIZABLE
:是否讀取其餘事務提交添加後的數據,解決幻影讀問題事務的傳播行爲
REQUIRED:若是當前沒有事務,就新建一個事務;若是已經存在一個事務,加入到這個事務中(默認值)
SUPPORTS:支持當前事務,若是當前沒有事務,就以非事務方法執行(沒有事務)
MANDATORY:使用當前的事務,若是當前沒有事務,就拋出異常
REQUERS_NEW:新建事務,若是當前在事務中,把當前事務掛起。
NOT_SUPPORTED:以非事務方式執行操做,若是當前存在事務,就把當前事務掛起
NEVER:以非事務方式運行,若是當前存在事務,拋出異常
NESTED:若是當前存在事務,則在嵌套事務內執行。若是當前沒有事務,則執行 REQUIRED 相似的操做
超時時間
默認值是 -1,沒有超時限制;如需有,以秒爲單位進行設置
是不是隻讀事務
建議查詢時設置爲只讀
TransactionStatus 接口
/** * TransactionStatus接口描述了某個時間點上事務對象的狀態信息,包含有6個具體的操做 */ //刷新事務 void flush(); //獲取是否存在存儲點 boolean hasSavepoint(); //獲取事務是否完成 boolean isCompleted(); //獲取事務是否爲新的事務 boolean isNewTransaction(); //獲取事務是否回滾 boolean isRollbackOnly(); //設置事務回滾 void setRollbackOnly();
必備依賴:spring-jdbc-xxx 和 spring-tx-xxx 等
建立 spring 的配置文件並導入約束
準備數據庫表和實體類
編寫業務層接口和實現類
編寫 Dao 接口和實現類
以上按照項目需求編寫,關鍵是配置,我的理解是上面所寫的 AOP 事務的更強形式
編寫 bean.xml 配置
<!--配置一個事務管理器--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!--注入數據源--> <property name="dataSource" ref="dataSource"/> </bean> <!--配置事務--> <!--配置事務的通知並引用事務管理器(用於管理事務)--> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <!--配置事務的屬性--> <tx:attributes> <!--指定方法名稱:是核心業務方法 read-only:是否只讀事務,默認false isolation:指定隔離級別,默認值是使用的數據庫默認隔離級別 propagation:指定事務的傳播行爲 timeout:指定超時時間,默認值-1表示永不超時 rollback-for:指定會進行回滾的異常類型,未指定表示任何異常都回滾 no-rollback-for:指定不進行回滾的異常類型,未指定表示任何異常都回滾 --> <tx:method name="*" read-only="false" propagation="REQUIRED"/> <tx:method name="find*" read-only="true" propagation="SUPPORTS"/> </tx:attributes> </tx:advice> <!--aop切點表達式--> <aop:config> <!--配置切入點表達式--> <aop:pointcut id="pt1" expression="execution(* yh.service.impl.*.*(..))"/> <!--配置切入點表達和事務通知的關係--> <aop:advisor advice-ref="txAdvice" pointcut-ref="pt1"/> </aop:config>
對比前面指定通知位置或者使用環繞通知都或多或少須要手動去處理代理邏輯,從而控制控制事務的方法的執行順序。
而使用 Spring 事務控制器,配置一個事務通知後,咱們只需關聯切入點表達式和事務通知便可。
必備依賴:spring-jdbc-xxx 和 spring-tx-xxx 等
建立 spring 的配置文件並導入約束
準備數據庫表和實體類
建立業務層接口及其實現類,並使用符合語義的註解讓 spring 進行管理
建立持久層接口及其實現類,並使用符合語義的註解讓 spring 進行管理
配置步驟
總 JavaConfig 類
@Configuration @Import(value={jdbcConfig.class,JdbcTemplateConfig.class,TransactionManager.class}) @ComponentScan("yh") //建立spring容器時掃描的包 @EnableTransactionManagement //開啓基於註解的事務管理功能(與開啓aop支持不要混淆) public class SpringConfiguration { }
建立事務管理器配置類並注入數據源
public class TransactionManager { @Bean(name="txManager") public PlatformTransactionManager createTxManager(DataSource dataSource){ return new DataSourceTransactionManager(dataSource); } }
數據源、JdbcTemplate 的 JavaConfig:
@PropertySource("classpath:jdbc.properties") //引入外部的properties屬性文件 public class jdbcConfig { @Value("${driverClassName}") private String driver; @Value("${jdbc.url}") private String url; @Value("${jdbc.username}") private String username; @Value("${jdbc.password}") private String password; @Bean(name="dataSource") public DataSource createDataSource(){ DruidDataSource ds = new DruidDataSource(); ds.setDriverClassName(driver); ds.setUrl(url); ds.setUsername(username); ds.setPassword(password); return ds; } }
jdbc.properties 屬性文件:
driverClassName=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306?ssl=true&useUnicode=true&characterEncoding=utf8 jdbc.username=root jdbc.password=root
public class JdbcTemplateConfig { @Bean(name="jdbcTemplate") public JdbcTemplate ceeateJdbcTemplate(DataSource dataSource){ return new JdbcTemplate(dataSource); } }
在業務層使用 @Transactional
註解
@Service("accountService") @Transactional(readOnly = true,propagation = Propagation.SUPPORTS) public class AccountServiceImpl implements IAccountService { /** * 獲取dao對象 */ @Autowired private IAccountDao accountDao; /** * 轉帳方法 * Transactional註解與<tx:Advice/>標籤含義相同,配置事務通知 * 可用在接口、類、方法上,表示其支持事務 * 三個位置的優先級 方法>類>接口 */ @Override @Transactional(readOnly = false,propagation = Propagation.REQUIRED) public void transferAccount(String sourceName, String targetName, float money) { //獲取帳戶 Account source = accountDao.findByName(sourceName); Account target = accountDao.findByName(targetName); //修改帳戶金額 source.setMoney(source.getMoney()-money); target.setMoney(target.getMoney()+money); //將修改後的帳戶更新至數據庫 accountDao.updateAccount(source); // int i = 1/0;//模擬異常 accountDao.updateAccount(target); } }
測試
@RunWith(SpringJUnit4ClassRunner.class) //替換原有運行器 @ContextConfiguration(classes = SpringConfiguration.class) //指定容器配置來源 public class MyTest { @Autowired private IAccountService service; @Test public void test1(){ service.transferAccount("aaa","ccc",100); } }
基於純註解配置以上。
使用 Spring 事務管理,業務代碼全稱躺着任由擺佈,各層級沒有代碼侵入問題。