手动实现一个迷你版的AOP(实战增强版)

news/2024/12/5 12:05:10/
点击蓝色“java大数据修炼之道”关注我哟加个“星标”,每晚21:00,一起学技术

在正式进行aop模块的介绍之前,我们需要先弄懂一些基本的术语概念。

在软件业,AOP 为 Aspect Oriented Programming 的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。

利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

AOP可以用于解决什么问题?

代码复用,公共函数抽取,简化开发,业务之间的解耦;最典型的例子就是日志功能,因为日志功能往往横跨系统中的每个业务模块,使用 AOP 可以很好的将日志功能抽离出来。

目前已有的几款AOP框架技术:

  • AspectJ:基于源代码和字节码解包的一个编织器,用户需要使用的时候用到一门新的语言,因此Spring的Aop对AspectJ进行了一次封装。

  • AspectWerkz:AOP 框架,使用字节码动态编织器和 XML 配置

  • JBoss-AOP:基于拦截器和元数据的 AOP 框架,运行在 JBoss 应用服务器上,以及 AOP 中用到的一些相关技术实现

  • Javassist:Java 字节码操作类库,JBoss 的一个子项目

AOP基本概念

在介绍AOP技术之前,我们先来理清几个基本概念点:

Aspect(切面)

可以理解为是将业务代码抽出来的一个类,例如:

@Aspect
public class LogAspect {/** ... 这里面是相关方法的部分 省略大部分内容 ... **/
}

JoinPoint(连接点)

拦截点其实可以理解为下边这个参数:

Spring框架目前只支持方法级别的拦截,其实aop的连接点还可以有多种方式,例如说参数,构造函数等等。

PointCut(切入点)

可以理解为对各个连接点进行拦截对一个定义。

Advice(通知)

指拦截到连接点之后需要执行的代码。

分为了前置,后置,异常,环绕,最终

具体表现如下:

目标对象

代理的目标对象

织入(weave)

将切面应用到目标对象,并且导致代理对象创建的过程。weave是一个操作过程。

引入(introduction)

在不修改代码的前提下,引入可以在运行的时候动态天津一些方法或者字段。

Cglib如何实现接口调用的代理

首先我们定义一个基本的业务代码对象:

package org.idea.spring.aop.cglib;
/*** @Author linhao* @Date created in 3:56 下午 2021/5/6*/
public class MyBis {void doBus(){System.out.println("this is do bis");}
}

接着是目标对象的拦截:

package org.idea.spring.aop.cglib;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/*** 整体的一个调用流程其实就是:* @Author linhao* @Date created in 3:57 下午 2021/5/6*/
public class TargetInterceptor implements MethodInterceptor {@Overridepublic Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {System.out.println("==== intercept before ====");//从代理实例的方法调用返回的值。Object result = methodProxy.invokeSuper(o,objects);System.out.println("==== intercept after ====");return result;}
}

最后是测试代码的执行。

package org.idea.spring.aop.cglib;
import net.sf.cglib.core.DebuggingClassWriter;
import net.sf.cglib.proxy.Enhancer;
/*** @Author linhao* @Date created in 3:59 下午 2021/5/6*/
public class TestCglib {public static void main(String[] args) throws InterruptedException {//将cglib生成的字节码文件存放到这个目录下边,查看下会有什么东西System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"/Users/idea/IdeaProjects/framework-project/spring-framework/spring-core/spring-aop/cglib-class");Enhancer enhancer = new Enhancer();enhancer.setSuperclass(MyBis.class);enhancer.setCallback(new TargetInterceptor());MyBis myBis = (MyBis) enhancer.create();myBis.doBus();}
}

执行结果:上边代码中的TargetInterceptor中的intercept方法会在目标函数被调用之后自动进行回调操作,从而实现代理调用的效果。

cglib和jdk代理

cglib的代理模式和jdk实现的代理技术本质还是会有较大差异性,jdk要求被代理对象需要实现jdk内部的InvocationHandler接口才能进行接口回调操作,但是cglib对是否实现接口方面没有强制要求,而且其性能也比JDK自带的代理要高效许多。

cglib代理的原理

关于cglib的原理我只能简单地介绍一下,仔细看了下里面的内容点实在是太多了,如果一下子深入挖掘容易掉进坑,所以这里打算用些大白话的方式来介绍好了。

cglib实现代理的基本思路

1.对被调用对象进行一层包装,并且对方法建立索引。

2.当调用目标方法的时候,通过索引值去寻找并调用函数。

这里面详细细节点可以参考这篇博客:

https://www.cnblogs.com/cruze/p/3865180.html

根据这篇博客介绍的思路,我自己也简单实现了一个cglib类似的代理工具。代码地址见文末

难点:

如何给方法建立索引?如何根据索引调用函数?

这里贴出我自己的一些思考和输出。

对调用对方法名称取出hashcode,然后通过switch关键字判断需要调用对函数名称:使用起来差不多,不过很多细节方面没有做得特别完善:使用cglib实现代理功能,主要目的就是希望在执行某些函数之前去调用某些方法。为了实现这种方式,其实借助反射也是可以达成目的的。但是反射在多次调用的时候性能开销比较大。cglib在这块所做的优化主要是对调用方法做了一次索引的包装,生产新的字节码,实现性能上的提升。

相关实现对代码仓库地址可以见文末。

Cglib底层是如何生成字节码文件的

ASM

对于需要手动操纵字节码的需求,可以使用ASM,它可以直接生产 .class字节码文件,也可以在类被加载入JVM之前动态修改类行为。ASM的应用场景有AOP(Cglib就是基于ASM)、热部署、修改其他jar包中的类等。

整体的操作流程图如下所示:

先通过ClassReader读取编译好的.class文件

其通过访问者模式(Visitor)对字节码进行修改,常见的Visitor类有:对方法进行修改的MethodVisitor,或者对变量进行修改的FieldVisitor等

通过ClassWriter重新构建编译修改后的字节码文件、或者将修改后的字节码文件输出到文件中

如何自己实现一个简单版本的AOP

首先需要定义相关的注解:

package org.idea.spring.aop.version1.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/*** @Author linhao* @Date created in 3:49 下午 2021/5/6*/
@Retention(value = RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Pointcut {String value() default "";
}
package org.idea.spring.aop.version1.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/*** @Author linhao* @Date created in 3:42 下午 2021/5/6*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Before {String value();
}
package org.idea.spring.aop.version1.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/*** @Author linhao* @Date created in 3:43 下午 2021/5/6*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface After {String value();
}
package org.idea.spring.aop.version1.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/*** @Author linhao* @Date created in 3:41 下午 2021/5/6*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Aspect {String value() default "";
}

接下来为自己定义的这些注解添砖加瓦,组合使用到一个Aspect的切面当中去:

package org.idea.spring.aop.version1.aspect;
import org.idea.spring.aop.version1.annotation.After;
import org.idea.spring.aop.version1.annotation.Aspect;
import org.idea.spring.aop.version1.annotation.Before;
import org.idea.spring.aop.version1.annotation.Pointcut;
import java.lang.reflect.Method;
/*** 切面** @Author linhao* @Date created in 3:43 下午 2021/5/6*/
@Aspect
public class MyAspect {@Pointcut("org.idea.spring.aop.version1.test.*.*(..)")public void pointCut(){}@Before("pointCut()")public void doBefore(Method method, Object object){System.out.println("doBefore");}@After("pointCut()")public void doAfter(Method method, Object object){System.out.println("doAfter");}
}

同时补充一个测试使用的方法

package org.idea.spring.aop.version1.test;
/*** @Author linhao* @Date created in 3:44 下午 2021/5/6*/
public class TestMethod {public void doTest(){System.out.println("do test");}
}

最后是一个核型的AspectLoader加载器代码

package org.idea.spring.aop.version1;
import com.google.common.collect.ImmutableSet;
import com.google.common.reflect.ClassPath;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import org.idea.spring.aop.version1.annotation.After;
import org.idea.spring.aop.version1.annotation.Aspect;
import org.idea.spring.aop.version1.annotation.Before;
import org.idea.spring.aop.version1.annotation.Pointcut;
import org.idea.spring.aop.version1.test.TestMethod;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/*** @Author linhao* @Date created in 3:51 下午 2021/5/6*/
public class AspectLoader {/*** 配置扫描aop的aspect基础包路径*/public static final String PACKAGE_NAME = "org.idea.spring.aop";/*** 模拟ioc容器*/public Map<String, Object> beanContainer = new HashMap<>();public AspectLoader() {this.beanContainer.put("TestMethod", new TestMethod());}public static void main(String[] args) {AspectLoader aspectLoader = new AspectLoader();aspectLoader.init();TestMethod testMethod = (TestMethod) aspectLoader.beanContainer.get("TestMethod");testMethod.doTest();}/*** 初始化aop的配置相关*/private void init() {try {//获取切面点aspectList<Class> targetsWithAspectJAnnotationList = this.getAspectClass();for (Class targetsWithAspectJAnnotation : targetsWithAspectJAnnotationList) {Method beforeMethod = this.getBeforeMethod(targetsWithAspectJAnnotation);Pointcut pointcut = (Pointcut) this.getMethodAnnotation(targetsWithAspectJAnnotation, Pointcut.class);Method afterMethod = this.getAfterMethod(targetsWithAspectJAnnotation);List<Class> classList = this.getClassFromPackage(AspectLoader.class, pointcut.value().substring(0, pointcut.value().indexOf("*") - 1));for (Class sourceClass : classList) {Object aspectObject = targetsWithAspectJAnnotation.newInstance();Enhancer enhancer = new Enhancer();enhancer.setSuperclass(sourceClass);enhancer.setCallback(new MethodInterceptor() {@Overridepublic Object intercept(Object obj, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {beforeMethod.invoke(aspectObject, method, obj);methodProxy.invokeSuper(obj, objects);afterMethod.invoke(aspectObject,method,obj);return obj;}});Object proxyObj = enhancer.create();this.beanContainer.put(sourceClass.getSimpleName(), proxyObj);}}} catch (ClassNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();} catch (InstantiationException e) {e.printStackTrace();}}private List<Class> getAspectClass() throws ClassNotFoundException, IOException {final ClassPath classPath = ClassPath.from(AspectLoader.class.getClassLoader());List<Class> aspectClass = new ArrayList<>();ImmutableSet<ClassPath.ClassInfo> clazz = classPath.getAllClasses();List<ClassPath.ClassInfo> list = clazz.asList();for (ClassPath.ClassInfo classInfo : list) {if (classInfo.getName() != null && classInfo.getPackageName().contains(PACKAGE_NAME)) {Class clazzTemp = Class.forName(classInfo.getName());if (clazzTemp.isAnnotationPresent(Aspect.class)) {aspectClass.add(clazzTemp);}}}return aspectClass;}/*** 获取指定包名下边的所有类** @param source* @param packageName* @return* @throws Exception*/private List<Class> getClassFromPackage(Class source, String packageName) {List<Class> classList = new ArrayList<>();final ClassPath classPath;try {classPath = ClassPath.from(source.getClassLoader());ImmutableSet<ClassPath.ClassInfo> clazz = classPath.getAllClasses();List<ClassPath.ClassInfo> list = clazz.asList();for (ClassPath.ClassInfo classInfo : list) {if (classInfo.getName() != null && classInfo.getPackageName().contains(packageName)) {classList.add(Class.forName(classInfo.getName()));}}} catch (IOException | ClassNotFoundException e) {e.printStackTrace();}return classList;}private Annotation getMethodAnnotation(Class source, Class annotationClass) {Method[] methods = source.getDeclaredMethods();for (Method method : methods) {if (method.isAnnotationPresent(annotationClass)) {Annotation[] beforeArr = method.getAnnotationsByType(annotationClass);if (beforeArr.length > 0) {return beforeArr[0];}}}return null;}private Method getBeforeMethod(Class source) {Method[] methods = source.getDeclaredMethods();for (Method method : methods) {if (method.isAnnotationPresent(Before.class)) {return method;}}return null;}private Method getAfterMethod(Class source) {Method[] methods = source.getDeclaredMethods();for (Method method : methods) {if (method.isAnnotationPresent(After.class)) {return method;}}return null;}
}

本文相关代码地址:

https://gitee.com/IdeaHome_admin/spring-framework-learn/tree/master/spring-aop

PS:如果觉得我的分享不错,欢迎大家随手点赞、在看。
▽加我微信,交个朋友
长按/扫码添加↑↑↑
1、速看: 加解密、加签验签,你想要的都在这了2、面试官欺负人:new Object()到底占用几个字节?3、为什么使用Token方式实现用户身份鉴权认证?4、如何快速定位当前数据库消耗CPU最高的sql语句?5、MySQL老大难事务和锁,一次性讲清楚!6、面试官问:缓存与数据库一致性如何解决?先操作数据库,还是缓存?

看完本文有收获?请转发分享给更多人

请长按二维码,关注 Java大数据修炼之道.

推荐程序员必备公众号 

Java大数据修炼之道

公众号:

gh_9119f24d3793

推荐理由:

在这里,我们分享程序员相关技术,职场生活,行业热点资讯。不定期还会分享IT趣文和趣图。这里属于我们程序员自己的生活,工作和娱乐空间。

 ▼长按下方↓↓↓二维码识别关注

关注 java大数据修炼之道

    每天学习java技术你想学的Java知识这里都有

点「在看」的人都涨薪了哦



http://www.ppmy.cn/news/218159.html

相关文章

迷你版jQuery——zepto核心源码分析

前言 zepto号称迷你版jQuery&#xff0c;并且成为移动端dom操作库的首选 事实上zepto很多时候只是借用了jQuery的名气&#xff0c;保持了与其基本一致的API&#xff0c;其内部实现早已面目全非&#xff01; 艾伦分析了jQuery&#xff0c;小钗暂时没有那个本事分析jQuery&#x…

手写一个迷你版Spring MVC框架

前期准备 我这里要写的是一个迷你版的Spring MVC&#xff0c;我将在一个干净的web工程开始开发&#xff0c;不引入Spring&#xff0c;完全通过JDK来实现。 我们先来看一眼工程&#xff1a; 工程代码结构 第一&#xff1a;在annotation包下&#xff0c;我将提供自定义的注解&…

极光im java_java手写一个迷你版的Tomcat代码分享

前言 Tomcat&#xff0c;这只3脚猫&#xff0c;大学的时候就认识了&#xff0c;直到现在工作中&#xff0c;也常会和它打交道。这是一只神奇的猫&#xff0c;今天让我来抽象你&#xff0c;实现你&#xff01; Write MyTomcat Tomcat是非常流行的Web Server&#xff0c;它还是一…

数据的异构实战(二)手写迷你版同步工程

上一期讲到了通过canal订阅mysql的binlog日志并且转换为对象&#xff0c;那么这一次我们将订阅来的对象通过RocketMQ发送消息&#xff0c;接收方接受消息之后同时存储到其他类型的数据源当中&#xff0c;完成一个简单的数据异构的过程。 什么是Java消息服务&#xff1f; 两个应…

shell迷你版监控脚本

写了一个shell版mini监控脚本&#xff0c;纯属跟大家分享学习下,有兴趣学习shell的可以看下。(*_*) 总共3个脚本文件service.sh, daemon.sh ,tcp.sh 实现了一些基本功能&#xff1a;端口监控&#xff0c;存活监控&#xff0c;掉线邮件报警&#xff0c;重新上线邮件通知 将3个文…

CentOS7迷你版安装Redis并配置基础信息

1. 安装gcc、wget依赖 yum install gcc yum install wget 2. 使用命令&#xff1a;wget http://download.redis.io/releases/redis-6.2.5.tar.gz 下载安装包&#xff0c;注意要先cd到要下载到的目标位置&#xff09; 3. tar -zxvf redis-6.2.5.tar.gz 解压压缩包 4. cd redis-…

用 java 写一个迷你版的 Tomcat

点击“终码一生”&#xff0c;关注&#xff0c;置顶公众号 每日技术干货&#xff0c;第一时间送达&#xff01; 1、前言 Tomcat&#xff0c;这只3脚猫&#xff0c;大学的时候就认识了&#xff0c;直到现在工作中&#xff0c;也常会和它打交道。这是一只神奇的猫&#xff0c;今…

实战干货:基于Redis6.0 部署迷你版本消息队列

技术研究背景 由于目前的研发团队处于公司初创阶段&#xff0c;尚未有能成熟的运维体系&#xff0c;对于市面上常见的成熟MQ搭建维护能力不足&#xff0c;但是又希望能有一款轻量级的消息系统供研发团队的成员使用&#xff0c;因此开展了对该方面相关的技术调研工作。 通过相关…