目前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设计模式 第二版》