動態代理
代理模式:給一個對象提供一個代理,並由代理對象來控制真實對象的訪問(調用者並不知道真實對象是什麼)。
代理模式分靜態代理和動態代理。這裏只討論動態代理,通俗的講,動態代理就是在不修改代碼的基礎對被代理對象進行方法的增強。
基於接口的動態代理
JDK自帶的動態代理就是基於接口的動態代理,被代理對象至少要實現一個接口,否則就無法使用代理。底層還是基於Java的反射來創建代理對象的。
JDK動態代理主要涉及兩個類,java.lang.reflect.Proxy
和 java.lang.reflect.InvocationHandler
。下面來看個例子。
被代理類的接口IProduct
public interface IProduct {
void saleProduct(Float money);
}
被代理類Product實現上面這個接口
public class Product implements IProduct{
@Override
public void saleProduct(Float money) {
System.out.println("賣出一個產品,收到金額:" + money);
}
}
編寫一個客戶端類獲取一個Product的動態代理的對象,來控制被代理對象的訪問。 InvocationHandler的實現主要通過內部類實現(可以直接使用lambda表達式更簡潔)。
public class Client {
public static void main(String[] args){
Product product = new Product();
// 基於接口的動態代理(jdk自帶的)
// proxyProduct就是Proxy.newProxyInstance生成的代理對象,需要強轉爲被代理對象的接口類。
// 參數:
// ClassLoader:代理對象的類加載器。使用和被代理對象相同的類加載器,直接調用getClass().getClassLoader()獲取。
// Class<?>[]:被代理對象的接口的字節碼。直接調用getClass().getInterfaces()獲取就行
// InvocationHandler:進行代碼的增強。直接使用內部類或者new一個InvocationHandler的實現類(在實現類中進行代碼增強)
IProduct proxyProduct = (IProduct) Proxy.newProxyInstance(product.getClass().getClassLoader()
, product.getClass().getInterfaces(), new InvocationHandler() {
/**
* 進行代理增強的方法
* 參數:
* proxy:當前的代理對象
* method:被代理對象當前執行的方法
* args:被代理對象當前執行的方法的參數
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object res = null;
// 獲取方法執行的參數
Float money = (Float)args[0];
// 判斷當前方法是否是銷售方法
if ("saleProduct".equals(method.getName())){
// 返回值與被代理對象方法的相同
res = method.invoke(product, money * 0.8f);
}
return res;
}
});
// 調用這個代理對象,執行被代理對象的方法。
proxyProduct.saleProduct(1000f);
}
}
基於子類的動態代理
CGLIB動態代理是一個基於ASM字節碼操作框架的代碼生成庫,它被廣泛用於基於代理的AOP框架提供方法攔截。
本質上說,對於需要被代理的類,它只是動態的生成一個子類以覆蓋非final的方法(所以它不能代理final類或final方法),同時綁定鉤子回調自定義的攔截器。它比使用Java反射的JDK動態代理方法更快。
CGLIB動態代理的核心是net.sf.cglib.proxy.Enhancer類(用於創建被代理對象子類),net.sf.cglib.proxy.MethodInterceptor是最常用的回調類型(它是net.sf.cglib.proxy.Callback的一個子接口)。
使用前需要引入CGLIB的jar依賴。
<!--cglib動態代理的依賴-->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.12</version>
</dependency>
下面來看一個示例(被代理對象還是和上面的相同,這裏實現一個客戶端類)。
public class Client {
public static void main(String[] args){
Product product = new Product();
/**
* 基於子類的動態代理(cglib)
* Enhancer的create方法,通過Enhancer來創建代理對象,MethodInterceptor來增強方法
* 參數:
* Class:被代理對象的字節碼
* Callback:用於增強方法。一般使用這個接口的子接口MethodInterceptor。
*/
Product proxyProduct = (Product) Enhancer.create(product.getClass(), new MethodInterceptor() {
/**
* 執行被代理對象的任何方法都會經過此方法
* @param proxy 當前代理對象
* @param method 被代理對象要執行的方法
* @param args1 要執行的方法的參數
* 以上三個參數與基於接口的動態代理(jdk)的參數一致
* @param methodProxy 當前執行方法的代理對象 一般使用這個對象來調用原始對象的方法,因爲它性能更高
* @return 與被代理對象要執行的方法的返回值一致
* @throws Throwable
*/
@Override
public Object intercept(Object proxy, Method method, Object[] args1, MethodProxy methodProxy) throws Throwable {
Object res = null;
// 獲取方法執行的參數
Float money = (Float)args1[0];
// 判斷當前方法是否是銷售方法
if ("saleProduct".equals(method.getName())){
// 修改原始方法的參數
args1[0] = money * 0.8f;
// 與JDK動態代理的一個區別,使用MethodProxy對象來調用原始對象的方法
res = methodProxy.invoke(product, args1);
}
return res;
}
});
// 通過代理對象執行被代理對象的方法
proxyProduct.saleProduct(1000f);
}
}
spring AOP
spring AOP是基於動態代理實現的,spring默認使用JDK動態代理實現AOP,也可以強制使用CGLIB。AOP是IoC的一種補充,IoC不依賴與AOP,但是AOP依賴與IoC。
AOP的一些概念
- JoinPoint(連接點):指被攔截到的點。在spring AOP中,就是指那些被攔截的方法。
- PointCut(切入點):是指篩選出的連接點(因爲所有的切入點都是連接點,但是有的連接點並不需要進行攔截增強方法)
- Advice(通知/增強):需要完成的工作叫做通知,就是寫的業務邏輯代碼中的公共代碼,比如事務、日誌等。通知的類型有五種。
- Before(前置通知):在連接點(攔截的方法)執行前的通知。
- AfterReturning(後置通知):在連接點正常完成返回後的通知。
- AfterThrowing(異常通知):如果方法執行過程中拋出了異常,則執行此通知。
- After(最終通知):無論方法執行是否成功,最終都要執行這個通知。
- Around(環繞通知):圍繞連接點的通知,它是最常用的通知,因爲它可以包括以上四種通知。
- Aspect(切面):其實就是通知和切點的結合,通知和切點共同定義了切面的全部內容,它是幹什麼的,什麼時候在哪執行。
- Introduction(引入):在不改變一個現有類代碼的情況下,爲該類添加屬性和方法,可以在無需修改現有類的前提下,讓它們具有新的行爲和狀態。
- Target(目標):被通知的對象。也就是需要加入額外代碼的對象,也就是真正的業務邏輯被組織織入切面。
- Weaving(織入):切面在指定的連接點被織入到目標對象中,在目標對象的生命週期裏有多個點(編譯期,類加載期,運行期)可以進行織入,spring是在運行期織入的。
@Aspectj支持
Spring 使用 AspectJ 提供的 library 解釋與 AspectJ 5 相同的註釋,用於切入點解析和匹配。但是,AOP 運行時仍然是純粹的 Spring AOP,並且不依賴於 AspectJ 編譯器或編織器。(也就是spring只是利用Aspectj來對切入點進行解析和匹配,真正的AOP的執行還是使用Spring AOP)。
在springboot中使用spring AOP非常的方便,只需引入一個spring支持AspectJ的starter依賴即可。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
在spring中也很簡單,在引用spring AOP的基礎上再引入一個aspectjweaver依賴就行。
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.2</version>
</dependency>
在spring中,要使用 Java @Configuration啓用 @AspectJ 支持,還需添加@EnableAspectJAutoProxy註解(在springboot中不用)。
下面來看一個簡單的示例(基於springboot)。
先寫個Service類(包括接口和實現類)。
public interface AccountService {
Account findById(Integer aid);
}
@Service
public class AccountServiceImpl implements AccountService {
private static final Logger log = LoggerFactory.getLogger(AccountServiceImpl.class);
@Override
public Account findById(Integer aid) {
log.info("正在執行方法");
return accountMapper.findById(aid);
}
}
一個配置類,對service中的方法進行切面。
@Aspect // 聲明切面, 這個bean將被spring自動檢測,並用於配置Spring AOP
@Component // 將這個類註冊爲一個bean,交給spring來管理(所以spring AOP依賴於IoC)
public class Logger {
private static org.slf4j.Logger log = LoggerFactory.getLogger(Logger.class);
/**
* 通用化切入點表達式,其他切入點可以直接引用這個
*/
@Pointcut("execution(* tkmybatis.service.impl.*.*(..))")
public void pointCut(){
}
/**
* 前置通知,在切入點方法執行之前執行
*/
@Before("pointCut()")
public void printLogBefore(){
log.info("我在方法執行前");
}
/**
* 後置通知,方法正常返回後執行
*/
@AfterReturning("pointCut()")
public void printLogAfterReturning(){
log.info("我在方法執行正常返回後");
}
/**
* 異常通知,方法發生異常時執行
*/
@AfterThrowing("pointCut()")
public void printLogAfterThrow(){
log.info("我在方法執行拋出異常後");
}
/**
* 最終通知,無論方法是否執行正常,它都會在最後執行
*/
@After("pointCut()")
public void printLogAfter(){
log.info("我在方法執行最後");
}
@Around("pointCut()")
public Object printAround(ProceedingJoinPoint pjp){
Object res;
try{
Object[] args = pjp.getArgs();
log.info("我在方法執行前");
res = pjp.proceed(args);
log.info("我在方法執行正常返回後2");
return res;
}catch (Throwable t){
log.info("我在方法執行拋出異常後2");
throw new RuntimeException(t);
}finally {
log.info("我在方法執行最後");
}
}
}
對於切入點表達式execution(* tkmybatis.service.impl.*.*(..))
,這裏做一下解釋。
execution表達式本來有三個部分(可見類型 返回值 連接點的全限定名),這裏的可見類型可以省略。
-
- 代表返回值可以爲任何值。
- tkmybatis.service.impl代表service實現類所在的包(進一步還可以直接用* .來替代,表示所有包)。
- .* 代表這個包下的所有類。下一個.* 代表這個類下的所有方法(還可以用方法前綴來表示,比如find*)。
- 括號裏的 … 代表參數可以爲任意參數,可爲多個或無。
更多高級用法詳見Spring官方文檔 Spring AOP
Spring 事務管理
Spring支持聲明式的事務管理和編程式的事務管理。聲明式事務管理使業務代碼不受污染,一個普通的POJO對象,只要加上註解就可以獲得完全的事務支持。Spring推薦使用聲明式的事務管理。
而聲明式的事務管理就是基於Spring AOP的,其本質就是對要執行的方法進行攔截,在方法執行前加入一個事務,方法執行完成後根據情況判斷是提交事務還是回滾事務(拋出了異常)。
事務的隔離級別
通過org.springframework.transaction.annotation.Isolation 這個枚舉類來指定。
public enum Isolation {
// 這是默認值,表示使用底層數據庫的默認隔離級別。對於MySQL的InnoDB存儲引擎,它是REPEATABLE_READ,其它一般的都是READ_COMMITTED
DEFAULT(-1),
// 跟沒有一樣,幾乎不使用。
READ_UNCOMMITTED(1),
// 只能讀取另一個事務已提交的事務,能防止髒讀。也是一般數據庫的默認隔離級別。
READ_COMMITTED(2),
// 可重複讀(在一個事務內多次查詢的結果相同,其它事務不可修改該查詢條件範圍內的數據,相當於加了個讀鎖)
REPEATABLE_READ(4),
// 所有的事務依次逐個執行,相當於串行化了,效率太低,一般也不使用。
SERIALIZABLE(8);
}
事務的傳播行爲
如果在開始當前事務之前,一個事務上下文已經存在,此時有若干選項可以指定一個事務性方法的執行行爲。org.springframework.transaction.annotation.Propagation枚舉類中定義了7表示傳播行爲的枚舉值。
public enum Propagation {
// 如果當前存在事務,則加入該事務;如果當前沒有事務,則創建一個新的事務。
REQUIRED(0),
// 如果當前存在事務,則加入該事務;如果當前沒有事務,則以非事務的方式繼續運行。
SUPPORTS(1),
// 如果當前存在事務,則加入該事務;如果當前沒有事務,則拋出異常。
MANDATORY(2),
// 創建一個新的事務,如果當前存在事務,則把當前事務掛起。
REQUIRES_NEW(3),
// 以非事務方式運行,如果當前存在事務,則把當前事務掛起。
NOT_SUPPORTED(4),
// 以非事務方式運行,如果當前存在事務,則拋出異常。
NEVER(5),
// 如果當前存在事務,則創建一個事務作爲當前事務的嵌套事務來運行;如果當前沒有事務,則該取值等價於REQUIRED。
NESTED(6);
}
聲明式事務的使用
聲明式事務可以直接使用@Transactional註解(這裏只討論這個)來配置,當然也可以使用XML配置文件。
先來看一下@Transactional註解都有啥
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
// 別名爲transactionManager,其實這兩是同一個。就是事務的名字。
@AliasFor("transactionManager")
String value() default "";
// 事務的傳播行爲,默認值爲REQUIRED
Propagation propagation() default Propagation.REQUIRED;
// 事務的隔離級別,默認爲默認值(也就是使用底層數據庫的隔離級別)
Isolation isolation() default Isolation.DEFAULT;
// 超時時間,默認爲 -1,也就是沒有超時時間。
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
// 是否只讀,默認爲false。
boolean readOnly() default false;
// 會觸發事務回滾的異常的字節碼數組
Class<? extends Throwable>[] rollbackFor() default {};
// 不會觸發事務回滾的異常的字節碼數組
Class<? extends Throwable>[] noRollbackFor() default {};
}
這裏直接在實現類上使用,這個註解還可以在接口(一般不在接口上使用)和方法上使用(可見性必須爲public,否則會被忽略)。
如果是在spring中使用,需要在配置類中加上@EnableTransactionManagement註解(springboot則不需要)。proxy-target-class屬性可以控制動態代理的類型,如果值爲false或忽略此屬性,則使用JDK的動態代理,可以設置爲true以強制使用CGLIB來進行動態代理(如果被代理的類沒有實現接口,將會自動使用CGLIB進行動態代理)。
@Service
@Transactional(isolation = Isolation.REPEATABLE_READ, rollbackFor = Exception.class) // 這裏觸發回滾的異常設置爲Exception
public class AccountServiceImpl implements AccountService {
}
參考:
CGLIB動態代理介紹
Spring AOP
Spring AOP的使用
Spring 事務管理
Spring 事務