目前Java的動態代理主要分爲jdk自帶的動態代理java.lang.reflect.Proxy 和 谷歌的cglib,它們有什麼區別呢?
原理:
- java動態代理是利用反射機制生成一個實現代理接口的匿名類,在調用具體方法前調用InvokeHandler來處理。
- 而cglib動態代理是利用asm開源包,對代理對象類的class文件加載進來,通過修改其字節碼生成子類來處理。
差別:
- 如果目標對象實現了接口,默認情況下會採用JDK的動態代理實現AOP
- 如果目標對象實現了接口,可以強制使用CGLIB實現AOP
- 如果目標對象沒有實現了接口,必須採用CGLIB庫,spring會自動在JDK動態代理和CGLIB之間轉換
JDK動態代理和CGLIB字節碼生成的區別?
- JDK動態代理只能對實現了接口的類生成代理,而不能針對類
- CGLIB是針對類實現代理,主要是對指定的類生成一個子類,覆蓋其中的方法
- 因爲是繼承,所以該類或方法最好不要聲明成final
Cglib比JDK快
- cglib底層是ASM字節碼生成框架,但是字節碼技術生成代理類,在JDL1.6之前比使用java反射的效率要高,在jdk6之後逐步對JDK動態代理進行了優化,在調用次數比較少時jdk勝出,只有在大量調用的時候cglib勝出,但是在1.8的時候JDK的效率已高於cglib
- Cglib不能對聲明final的方法進行代理,因爲cglib是動態生成代理對象,final關鍵字修飾的類不可變只能被引用不能被修改
JDK動態代理
JDK動態代理介紹:
- 生成的代理對象不需要實現接口,但是目標對象要實現接口,否則無法使用動態代理,因爲在轉化過程中會拋異常
- 代理對象的生成是利用JDK的api,動態的內存中構建代理對象
JDK中生成的代理對象api
- 代理類包:java.lang.reflect.Proxy
- jdk實現代理只需要使用Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
jdk動態代理 它允許我們用代理類來包裝其它類,也就是說可以利用代理對象來攔截被包裝對象的請求,當然也可以在攔截前後加上自己的邏輯(環繞增強),對動態代理施加限制可以防止包裝任意對象,在正常條件下動態代理能夠完整的控制被包裝對象的控制。爲創建動態代理,需要遵循一下三點:
- 類加載器與當代理攔截調用時需要執行的行爲類,可以使用需要包裝的相關對象來獲取一個合適的類加載器:ClassLoader loader = obj . getClass () .getClassLoader;因爲它要幫你創建一個代理對象,對象的創建肯定是通過類來創建的,此時Proxy就是那個代理工廠類,而這個代理類就是通過類加載器加載進來,然後在創建其實例。
- 必須列出所要攔截的接口列表 :Class[] classes = obj.getClass().getInterfaces() 這行代碼可以獲得所要攔截的方法實現的所有接口,那爲什麼需要這個接口呢?比如:假設你原來的行爲類爲a,a的接口爲ai,現在你利用加載器創建了一個代理類ap,而這個代理類顯然內部必須持有a,但是問題是要用代理類ap來觸發a接口的雖然可以做到但是這樣就失去了代理的意義,必須是你替a把這件事情做了,所以除了加載器還需要聲明a所實現的接口,這樣ap也會實現此接口,現在a實現了ai接口,ap也實現了ai接口,那ap就能向上轉型爲ai(多態),這樣ap持證上崗,去做ai該接口的事情了。
- 最後一個需要的元素是代理對象自身,這個對象的類型必須實現java. lang. reflect包中的Invocati onHandler接口。該接口聲明瞭如下操作:public object invoke(object proxy, Method m,Object[] args) throws Throwable;而這裏就是去實現具體的邏輯
其中第三點非常關鍵,那是你實現業務增強的切面,在對動態代理進行包裝時,對包裝對象的調用會轉發給你所提供類的invoke()方法,
invoke()方法會繼續將方法調用轉發給被包裝對象。可以通過如下代碼轉發調用:object out_args = method.invoke(targetObj, in_args);這行代碼通過反射將目標調用轉發給被包裝對象,動態代理的美妙在於可以在轉發調用之前或者之後執行任何行爲。
上面的圖解就好比,我要去賣一些產品,但是由於銷量不佳,就找到網紅,想讓他幫幫我,然後網紅在賣產品直接可能就會去打廣告啊、直播啊等等,等到做完這些事情了,然後網紅就會說萬事俱備只欠東風,便可以網上預售了 ,然後又把賣產品的任務重新返還給了我,然後我賣完產品後得到一些收益,網紅看我收益不行可能又會幫我做一些其它的廣告啥的,直至這些業務邏輯結束!
我就是那個目標類,網紅就是代理類。目標類要去做賣產品的動作,但會被代理類所攔截,然後代理類做一些業務需要實現的邏輯,做完之後又將賣產品的動作交目標類,目標類利用反射去實現賣產品動作(或許會返回一個結果),代理可能又會對着個結果乾嘛幹嘛。直至結束。
動態代理實現一:方法環繞增強
舉例:有一個動物接口,接口中有兩個方法,然後很多類都需要實現它並且複寫這兩個方法,但是我們想要在執行這個方法之前執行下自己自定義的一個方法,怎麼做?那麼這就可以利用動態代理去實現。
1.首先定義一個接口類,並且內部類dog去實現這個方法
public interface Animals {
void run();
String eat(String food);
static class Dog implements Animals{
@Override
public void run() {
System.out.println ("狗開始跑");
}
@Override
public String eat(String food) {
System.out.println ("狗開始喫" + food);
return "喫" + food;
}
}
}
2.在定義一個測試類,當然因爲要用到動態代理,所以要用到InvocationHandler這個類,並且複寫這個類的invoke(Object proxy, Method method, Object[] args)方法,此方法代理塊裏就可以填寫的你的動態代理的業務邏輯了。
package Aop.JDKProxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class AnimalsProxy implements InvocationHandler {
private Object target;
//使用動態代理必須傳入目標類
public AnimalsProxy(Object target){
this.target = target;
}
/**
* @proxy 被代理的對象
* @method 要調用的方法
* @agrs 方法調用所需參數
* */
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//前置增強
command1();
//執行被代理對象的方法、如果有返回值則賦值給ret
Object ret = method.invoke (target,args);
//後置增強
command2();
return ret;//方法執行完成返回的結果,可用到後續的結果在做處理類比Spring的註解@AfterReturning
}
//定義要插入的方法
private void command1(){
System.out.println ("---主人發出命令---");
}
private void command2(){
System.out.println ("---主人給出獎勵---");
}
public static void main(String[] args) {
Animals animalsTarget = new Animals.Dog ();
Animals animalsProxy = (Animals) Proxy.newProxyInstance (animalsTarget.getClass ().getClassLoader (),animalsTarget.getClass ().getInterfaces (),new AnimalsProxy(animalsTarget));
//Exception in thread "main" java.lang.ClassCastException: com.sun.proxy.$Proxy0 cannot be cast to Aop.JDKProxy.Animals
animalsProxy.run ();
animalsProxy.eat ("骨頭");
}
}
---主人發出命令---
狗開始跑
---主人給出獎勵---
---主人發出命令---
狗開始喫骨頭
---主人給出獎勵---
需要注意的就是在調用 Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h),目標類必須實現接口否則會拋出類轉化異常,因爲返回的是一個代理類對象 $Proxy0@502。
看完上面一個簡單的demo之後,想必對動態代理有了簡單的認知,那還會在其它哪些領域上用上呢?go on!
動態代理實現(二):事務
mybaties的事務也是利用動態代理實現的,現在就用jdk的方式寫一些僞代碼去簡單實現下。
//接口
public interface IUserService {
void saveUser();
}
//實現子類
public class UserServiceImpl implements IUserService {
@Override
public void saveUser() {
System.out.println("保存用戶信息");
}
}
package Aop.JDKProxy2;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class DynamicProxy implements InvocationHandler {
private Object targetObj;//目標對象
public DynamicProxy(Object targetObj){
this.targetObj = targetObj;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object obj = null;
try {
startTranstraction();
obj = method.invoke(targetObj, args);
}catch (Exception e){
rollback();
}finally {
conmmitTranstraction();
}
return obj;
}
public void startTranstraction(){
System.out.println("開啓事物");
}
public void conmmitTranstraction(){
System.out.println("提交事物");
}
public void rollback(){
System.out.println("回滾事物");
}
public static void main(String[] args) {
UserServiceImpl target = new UserServiceImpl();
DynamicProxy dynamicProxy = new DynamicProxy(target);
IUserService proxy = (IUserService) Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),dynamicProxy);
proxy.saveUser();
}
}
開啓事物
保存用戶信息
提交事物
動態代理實現(三):接口訪問次數和接口請求耗時
在我們項目中經常會用到接口、考究接口調用頻率和接口耗時,現在Java的項目大多是是利用Spring框架幫我們去實現各種功能,現在我們就利用Spring框架去幫我們實現這一個簡單的Aop功能。在實現之前有必要了解一下Spring關於aop實現常用的註解和基本知識。
AOP是Spring提供的兩個核心功能之一:IOC(控制反轉)、AOP(Aspect Oriented Programming 面向切面編程),IOC有助於應用對象之間的解耦,類似於我們常用的注入service類,而AOP可以實現橫切關注點和它所影響的對象之間的解耦,它通過對既有的程序定義一個橫向切入點,然後在其前後切入不同的執行內容,來拓展應用程序的功能,常見的用法如:打開事務和關閉事物,記錄日誌,統計接口時間等。AOP不會破壞原有的程序邏輯,拓展出的功能和原有程序是完全解耦的,因此,它可以很好的對業務邏輯的各個部分進行隔離,從而使業務邏輯的各個部分之間的耦合度大大降低,提高了部分程序的複用性和靈活性。實現aop切面,主要有以下幾個關鍵點需要了解:
切面(Aspect):用來切插業務方法的類:
- 連接點(joinpoint):是切面類和業務類的連接點,其實就是封裝了業務方法的一些基本屬性,作爲通知的參數來解析。
- 通知(advice):在切面類中,聲明對業務方法做額外處理的方法,Spring註解爲@Aspect
- 切入點(pointcut):業務類中指定的方法,作爲切面切入的點。其實就是指定某個方法作爲切面切的地方。在Spring中可以是規則表達式,也可以是某個package下的所有函數,也可以是一個註解等,其實就是執行條件,滿足此條件的就切入,註解爲@Pointcut
- 目標對象(target object):被代理對象。
- AOP代理(aop proxy):代理對象。
通知(Advice):也可以稱爲xx增強
- 前置通知(before advice):在切入點之前執行, Spring註解爲@Before
- 後置通知(after returning advice):在切入點執行完成後,執行通知,Spring註解爲@After
- 環繞通知(around advice):包圍切入點,調用方法前後完成自定義行爲,Spring註解爲@Around
- 異常通知(after throwing advice):在切入點拋出異常後,執行通知。Spring註解爲@AfterThrowing
- 返回通知(after return advice):對返回值進行處理則可以使用,無返回值則無需使用,Spring註解爲@AfterReturning
Aop在我們Spring框架中更是更是層出不窮,那麼必然會聽到一些常見的面試題?比如什麼是aop?而它又包含了什麼連接點、切入點、增強功能等。比如我們上面哪個程序:跑、喫就是我們的方法點,也就是連接點,而我們要在執行n多方法,增加我們自己的方法,比如在跑之前加一句“主人發出命令”,而跑方法前加的方法command1位置就是切入點,我們的command1()方法就是我們的增強功能,而兩者則統稱爲切面。
言歸正傳,我們對剛纔提出的需求利用spring框架給我們帶來的便利去解決上面的問題。
一:首先創建我們的接口
/**
* @author heian
* @create 2020-01-19-12:38 下午
* @description 模擬調用XX平臺結果
*/
public interface TestRpcInterface {
void queryXXX();
void queryYYY();
}
/**
* @author heian
* @create 2020-01-19-12:39 下午
* @description 模擬實現調用XX平臺耗時
*/
@Service
public class TestRpcInterfaceImpl implements TestRpcInterface{
private Logger logger = LoggerFactory.getLogger(TestRpcInterfaceImpl.class);
@Override
public void queryXXX() {
try {
logger.info("本體======>"+Thread.currentThread().getName() + "queryXXX:welcome to study aop");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void queryYYY() {
try {
logger.info("本體======>"+Thread.currentThread().getName() + "queryYYY:welcome to study aop");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
二:創建我們所需的切面類
package com.cloud.ceres.rnp.service.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author heian
* @create 2020-01-17-12:54 上午
* @description 切面類:單獨對AopControl類某個做接口統計訪問
*/
@Component
@Aspect
public class TestRpcInterfaceImplAspect {
//統計接口耗時
private ThreadLocal<Long> timeLocal = new ThreadLocal<>();
//統計接口訪問次數
private ThreadLocal<Integer> numLocal = new ThreadLocal<>();
private AtomicInteger atomicInteger = new AtomicInteger(0);
private static Logger logger = LoggerFactory.getLogger (TestRpcInterfaceImplAspect.class);
/**
* @description:定義一個切入點,可以是規則表達式,也可以是某個package下的所有函數方法,也可以是一個註解,其實就是執行條件,滿足此條件的就切入
* 備註:Controller層下的所有類的所有方法,返回類型任意,方法參數任意
* 從前到後順序解釋:execution 在滿足後面的條件的方法執行時觸發
* 第一個* 表示返回值任意,記得有空格
* com.cloud.ceres.rnp.control.web.AopControl.類.方法 表示此路徑下的任意類的任意方法
* (..) 表示方法參數任意
*舉例:對某個包下所有類所有方法 execution(* com.cloud.ceres.rnp.service.aspect.*.*(…))
*/
@Pointcut("execution(* com.cloud.ceres.rnp.service.aspect.TestRpcInterfaceImpl.*(..))")
public void myPointCut(){
}
@Around("myPointCut()")
public void printAroundLog(ProceedingJoinPoint joinPoint) throws Throwable {
numLocal.set(atomicInteger.incrementAndGet());
timeLocal.set(System.currentTimeMillis());
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
//記錄請求日誌
logger.info("前置======>url;"+request.getRequestURL().toString() + ",接口類型" + request.getMethod() + "接口ip:" + request.getMethod());
logger.info("前置======>classMethod : " + joinPoint.getSignature().getDeclaringTypeName() + "下的" + joinPoint.getSignature().getName() + "參數:" + Arrays.toString(joinPoint.getArgs()));
Object args = joinPoint.proceed();
String pkgName = joinPoint.getSignature ().getDeclaringTypeName ();
String methodName = joinPoint.getSignature ().getName ();
logger.info("後置======>" + pkgName +"下的" +methodName +
"方法耗時爲:" + (System.currentTimeMillis ()-timeLocal.get ()) + "毫秒"+ "訪問次數爲:" + numLocal.get ());
}
}
當然如果你要對返回值要處理則可以用以下代碼:
package com.cloud.ceres.rnp.service.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author heian
* @create 2020-01-17-12:54 上午
* @description 切面類:單獨對AopControl類某個做接口統計訪問
*/
@Component
@Aspect
public class TestRpcInterfaceImplAspect {
//統計接口耗時
private ThreadLocal<Long> timeLocal = new ThreadLocal<>();
//統計接口訪問次數
private ThreadLocal<Integer> numLocal = new ThreadLocal<>();
private AtomicInteger atomicInteger = new AtomicInteger(0);
private static Logger logger = LoggerFactory.getLogger (TestRpcInterfaceImplAspect.class);
/**
* @description:定義一個切入點,可以是規則表達式,也可以是某個package下的所有函數方法,也可以是一個註解,其實就是執行條件,滿足此條件的就切入
* 備註:Controller層下的所有類的所有方法,返回類型任意,方法參數任意
* 從前到後順序解釋:execution 在滿足後面的條件的方法執行時觸發
* 第一個* 表示返回值任意,記得有空格
* com.cloud.ceres.rnp.control.web.AopControl.類.方法 表示此路徑下的任意類的任意方法
* (..) 表示方法參數任意
*舉例:對某個包下所有類所有方法 execution(* com.cloud.ceres.rnp.service.aspect.*.*(…))
*/
@Pointcut("execution(* com.cloud.ceres.rnp.service.aspect.TestRpcInterfaceImpl.*(..))")
public void myPointCut(){
}
/**
* @description:在切入點之前執行
* @param joinPoint 切面類和業務類的連接點,其實就是封裝了業務方法的一些基本屬性,作爲通知的參數來解析。
* 備註:如果前置方法裏不需要連接點,joinPoint可以不傳入
*/
@Before("myPointCut()")
public void printBeforeLog(JoinPoint joinPoint){
int visitTimes = atomicInteger.incrementAndGet();
numLocal.set(visitTimes);
timeLocal.set(System.currentTimeMillis());
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
//記錄請求日誌
logger.info("前置======>url;"+request.getRequestURL().toString() + ",接口類型" + request.getMethod() + "接口ip:" + request.getMethod());
logger.info("前置======>classMethod : " + joinPoint.getSignature().getDeclaringTypeName() + "下的" + joinPoint.getSignature().getName() + "參數:" + Arrays.toString(joinPoint.getArgs()));
}
/**
* @param pointcut 切入點
* @param returning 接口方法返回的結果參數
* 備註:如果後置方法裏不需要連接點,joinPoint可以不傳入
*/
@AfterReturning(pointcut = "myPointCut()",returning = "args")
public void printBeforeLog(JoinPoint joinPoint,Object args){
logger.info("後置======>response:"+args);//處理返回值信息
String pkgName = joinPoint.getSignature ().getDeclaringTypeName ();
String methodName = joinPoint.getSignature ().getName ();
logger.info("後置======>" + pkgName +"下的" +methodName +
"方法耗時爲:" + (System.currentTimeMillis ()-timeLocal.get ()) + "毫秒"+ "訪問次數爲:" + numLocal.get ());
}
}
三:創建control層接口測試
@RestController
@RequestMapping("/aop")
public class AopControl {
@Autowired
private TestRpcInterfaceImpl testRpcInterfaceImpl;
private static Logger logger = LoggerFactory.getLogger (AopControl.class);
@GetMapping("/queryXXX")
public String queryXXX() {
testRpcInterfaceImpl.queryXXX();
return "aopTest方法結束";
}
@GetMapping("/queryYYY")
public String queryYYY() {
testRpcInterfaceImpl.queryYYY();
return "aopTest方法結束";
}
}
四:用IDEA自帶的客戶端工具進行調試(淘汰postman)
控制檯輸出信息如下:(我這裏爲了節省篇幅就調一次接口)
2020-01-19 14:21:08.216 INFO 5538 --- [nio-8000-exec-1] c.c.c.r.s.a.TestRpcInterfaceImplAspect : 前置======>url;http://localhost:8000/aop/queryYYY,接口類型GET接口ip:GET
2020-01-19 14:21:08.217 INFO 5538 --- [nio-8000-exec-1] c.c.c.r.s.a.TestRpcInterfaceImplAspect : 前置======>classMethod : com.cloud.ceres.rnp.service.aspect.TestRpcInterfaceImpl下的queryYYY參數:[]
2020-01-19 14:21:08.230 INFO 5538 --- [nio-8000-exec-1] c.c.c.r.s.aspect.TestRpcInterfaceImpl : 本體======>http-nio-8000-exec-1queryYYY:welcome to study aop
2020-01-19 14:21:13.235 INFO 5538 --- [nio-8000-exec-1] c.c.c.r.s.a.TestRpcInterfaceImplAspect : 後置======>com.cloud.ceres.rnp.service.aspect.TestRpcInterfaceImpl下的queryYYY方法耗時爲:2020毫秒訪問次數爲:1
思考:手寫切面類代替spring註解,實現動態代理嗎?
現在我們知道了動態代理的原理,同時也知道了反射可以幫我們轉發方法調用,那我們就自己手寫一個接口來完成上述操作,來代替Spring的註解或者說切面類。
- 創建行爲接口,我這裏是模擬rpc服務端調用,定義爲TestRpcInterface接口
- 創建實現該接口的行爲類或者目標類TestRpcInterfaceImpl
- 創建增強通知接口Advice,可以以後共給其它通知增強通知實現類
- 創建增強通知實現類AdviceImpl
- 創建切點Pointcut類,用來管理哪些類和方法可以得到增強
- 創建切面Aspect類 = Advice + Pointcut
- 創建容器工廠上下文類ApplicationContext,用來存放切面、行爲類對象和返回代理類
- 創建容器工廠上下文實現DefaultApplicationContext
- 創建代理類AopInvocationHandler
具體的代碼邏輯實現
1. 行爲接口
/**
* @author heian
* @create 2020-01-19-12:38 下午
* @description 模擬調用XX平臺結果
*/
public interface TestRpcInterface {
void queryXXX();
void queryYYY();
}
2. 行爲接口實現
/**
* @author heian
* @create 2020-01-19-12:39 下午
* @description 模擬實現調用XX平臺耗時
*/
@Service
public class TestRpcInterfaceImpl implements TestRpcInterface{
private Logger logger = LoggerFactory.getLogger(TestRpcInterfaceImpl.class);
@Override
public void queryXXX() {
try {
logger.info("本體======>"+Thread.currentThread().getName() + "queryXXX:welcome to study aop");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void queryYYY() {
try {
logger.info("本體======>"+Thread.currentThread().getName() + "queryYYY:welcome to study aop");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3. 增強通知接口
/**
* @author heian
* @create 2020-01-17-12:28 上午
* @description 增強接口 Spring中包含:前置增強、後置增強、環繞增強、返回增強、異常增強,
* 1.實現怎樣的增強邏輯,可以實現該接口,比如AdviceImpl就實現了環繞增強
* 2.具體實現可以通過AdviceInpl來實現
*/
public interface Advice {
Object invoke(Object target, Method method, Object[] args) throws Throwable;
}
4. 增強通知實現
package com.cloud.ceres.rnp.service.aop;
import com.cloud.ceres.rnp.control.aop.Advice;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.lang.reflect.Method;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author heian
* @create 2020-01-18-9:49 下午
* @description 業務所需的增強邏輯
*/
@Service
public class AdviceImpl implements Advice {
private Logger logger = LoggerFactory.getLogger(AdviceImpl.class);
//統計訪問次數
private ThreadLocal<Integer> visitTimes = new ThreadLocal<>();
//統計接口耗時
private ThreadLocal<Long> costTime = new ThreadLocal<>();
//次數自增
private AtomicInteger num = new AtomicInteger(0);
@Override
public Object invoke(Object target, Method method, Object[] args) throws Throwable {
visitTimes.set(num.incrementAndGet());
costTime.set(System.currentTimeMillis());
Object out_args = method.invoke(target, args);//方法調用返回結果值
logger.info("類名:" + target.getClass().getName() + ",方法名:" + method.getName() + ",接口耗時:" + (System.currentTimeMillis() - costTime.get() )+ "毫秒");
logger.info("類名:" + target.getClass().getName() + ",方法名:" + method.getName() + ",訪問次數:" + (visitTimes.get())+ "次");
return out_args;
}
}
5. 切點
package com.cloud.ceres.rnp.control.aop;
/**
* @author heian
* @create 2020-01-18-10:24 下午
* @description 正則表達式篩選指定類與方法
*/
public class Pointcut {
private String classPattern;
private String methodPattern;
public Pointcut(String classPattern, String methodPattern) {
this.classPattern = classPattern;
this.methodPattern = methodPattern;
}
public String getClassPattern() {
return classPattern;
}
public void setClassPattern(String classPattern) {
this.classPattern = classPattern;
}
public String getMethodPattern() {
return methodPattern;
}
public void setMethodPattern(String methodPattern) {
this.methodPattern = methodPattern;
}
}
6. 切面類
/**
* @author heian
* @create 2020-01-18-10:28 下午
* @description 整合之前所寫的pointcut + advice,便於後續使用
*/
public class Aspect {
private Advice advice;
private Pointcut pointcut;
public Aspect(Advice advice, Pointcut pointcut) {
this.advice = advice;
this.pointcut = pointcut;
}
public Advice getAdvice() {
return advice;
}
public void setAdvice(Advice advice) {
this.advice = advice;
}
public Pointcut getPointcut() {
return pointcut;
}
public void setPointcut(Pointcut pointcut) {
this.pointcut = pointcut;
}
}
7. 工廠上下文接口
/**
* @author heian
* @create 2020-01-18-10:54 下午
* @description 工廠接口
*/
public interface ApplicationContext {
void registBeanDefinition(String beanName,Class<?> beanClass);//存入目標對象
void setAspect(Aspect aspect);//存入切面
Object getBean(String beanName) throws IllegalAccessException, InstantiationException;//拿到代理對象
}
8. 工廠上下文實現
package com.cloud.ceres.rnp.control.ioc;
import com.cloud.ceres.rnp.control.aop.AopInvocationHandler;
import com.cloud.ceres.rnp.control.aop.Aspect;
import org.springframework.stereotype.Component;
import java.lang.reflect.Proxy;
import java.util.*;
/**
* @author heian
* @create 2020-01-18-10:56 下午
* @description 工廠類實現 返回你需要的對象
*/
@Component
public class DeafultApplicationContext implements ApplicationContext {
//存入target目標類對象 (很多源碼都是將一些信息 通過map方式存於內存中)
private Map<String,Class<?>> beanFactoryMap = new HashMap<>();
//存入切面對象
private Set<Aspect> aspects = new HashSet<>();
@Override
public Object getBean(String beanName) throws IllegalAccessException, InstantiationException {
//要獲得類,必須要制定規則,也就是Bean的定義信息
Object obj = null;
if (this.beanFactoryMap.get(beanName) != null){
Class<?> targetClass = this.beanFactoryMap.get(beanName);
//代理增強
obj = proxyEnhance(targetClass.newInstance());//通過反射獲取新對象,而不是讓用戶來new 對象
}
return obj;
}
/**
*
* @param beanName key:類名
* @param beanClass value:對應存入的目標對象的class
*/
@Override
public void registBeanDefinition(String beanName, Class<?> beanClass) {
beanFactoryMap.put(beanName,beanClass);
}
@Override
public void setAspect(Aspect aspect) {
this.aspects.add(aspect);
}
/**
* 判斷使用者 傳過來的bean對象是否屬於我們存在的切面中,在則創建代理對象,沒切中則直接返回反射創建的工廠類
* @param target
*/
private Object proxyEnhance(Object target){
//判斷切面是否存在集合中
for (Aspect aspect : this.aspects) {
//傳過來的類是否符合我們這個容器內存好的pointcut 這裏僅僅比較類名
if (target.getClass().getName().matches(aspect.getPointcut().getClassPattern())){
//System.out.println("該bean符合我們pointcut的範圍,開始創建代理類");
Object out_args = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new AopInvocationHandler(target, aspect));
return out_args;
}
}
return target;
}
}
9. 代理類
package com.cloud.ceres.rnp.control.aop;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
/**
* @author heian
* @create 2020-01-19-12:21 上午
* @description 代理類
*/
public class AopInvocationHandler implements InvocationHandler {
private Object targetObj; //目標對象
private Aspect aspect;//切面類
public AopInvocationHandler(Object targetObj, Aspect aspect) {
this.targetObj = targetObj;
this.aspect = aspect;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//執行增強邏輯
//執行目標方法,得到增強對象
if (method.getName().matches(this.aspect.getPointcut().getMethodPattern())) {
// System.out.println("進入代理類,已經知道目標對象屬於這個類,觀察其方法,是不是屬於正則的方法");
//執行增強邏輯
Object out_args = this.aspect.getAdvice().invoke(targetObj, method, args);
return out_args;
}
return method.invoke(targetObj,args);//沒有切中,則不執行增強邏輯,直接返回反射的方法即可
}
}
測試
package com.cloud.ceres.rnp.control.web;
import com.cloud.ceres.rnp.control.aop.Advice;
import com.cloud.ceres.rnp.control.aop.Aspect;
import com.cloud.ceres.rnp.control.aop.Pointcut;
import com.cloud.ceres.rnp.control.ioc.ApplicationContext;
import com.cloud.ceres.rnp.control.ioc.DeafultApplicationContext;
import com.cloud.ceres.rnp.service.aop.AdviceImpl;
import com.cloud.ceres.rnp.service.aspect.TestRpcInterface;
import com.cloud.ceres.rnp.service.aspect.TestRpcInterfaceImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author heian
* @create 2020-01-17-12:55 上午
* @description AOP測試Control層
*/
@RestController
@RequestMapping("/aop")
public class AopControl {
@Autowired
private TestRpcInterfaceImpl testRpcInterfaceImpl;
@Autowired
private DeafultApplicationContext deafultApplicationContext;
private AtomicInteger atomicInteger = new AtomicInteger(0);
private static Logger logger = LoggerFactory.getLogger (AopControl.class);
@GetMapping("/queryXXX")
public String queryXXX() {
testRpcInterfaceImpl.queryXXX();
return "aopTest方法結束";
}
@GetMapping("/queryYYY")
public String queryYYY() {
testRpcInterfaceImpl.queryYYY();
return "aopTest方法結束";
}
@GetMapping("/testMy")
public void testMy() throws Exception{
if (atomicInteger.incrementAndGet() == 1){
//第一次進來,需要
Advice adviceImpl = new AdviceImpl();
Pointcut pointcut = new Pointcut("com\\.cloud\\.ceres\\.rnp\\.service\\.aspect\\.TestRpcInterfaceImpl",".*");//.正則特殊字符,需要轉義
Aspect aspect = new Aspect(adviceImpl,pointcut);
//存入準備好的切面到工廠容器 類比:Spring是通過xml或者註解的形式)ioc是aop的基石
deafultApplicationContext.setAspect(aspect);
deafultApplicationContext.registBeanDefinition("TestRpcInterfaceImpl", TestRpcInterfaceImpl.class);
}
//通過之前存入切面作爲條件,判斷註冊到工廠類的bean是否處於Pointcut中,處於則返回代理類,不處於則直接返回工廠對象
TestRpcInterface testRpcInterface = (TestRpcInterface) deafultApplicationContext.getBean("TestRpcInterfaceImpl");//返回代理對象
testRpcInterface.queryXXX();
testRpcInterface.queryYYY();
}
}
通過調用testMy接口來觸發queryXXX、queryXXX接口的調用,需要注意的就是第一次訪問必須把切面類加載到deafultApplicationContext容器中,而deafultApplicationContext是我一開始就存在Spring的容器中,如果第一次不把切面類加載到deafultApplicationContext容器中,那我這整個就不能生效了。 連續調用兩次testMy接口
總結:
可以發現,我用Spring註解的形式實現接口耗時統計和訪問次數的統計就用了兩個類就簡單實現了,而自己要去不用框架提供的註解去自定義實現,確實費神費力,不過其中也讓我學習了很多Spring的底層的東西,但是我這裏ioc容器模塊調用了aop模塊實則是不是最佳實踐(底層調用上層),仍然需要優化。我們雖然整天寫的都是crud操作,但時刻要提醒自己,居安思危。
參考:https://blog.csdn.net/XiYoumengshen/article/details/87909861
https://blog.csdn.net/weixin_39800144/article/details/83013946
《java設計模式 第二版》