一文带你看清 AOP 所有概念!

2024-01-20 21:40
文章标签 概念 所有 aop 一文 看清

本文主要是介绍一文带你看清 AOP 所有概念!,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

开足码力,码动人生,微信搜索【 程序员大帝 】,关注这个一言不合就开车的的代码界老司机
本文 GitHub上已经收录 https://github.com/BeKingCoding/JavaKing , 一线大厂面试核心知识点、我的联系方式和技术交流群,欢迎Star和完善

前言

如果你是个 Java 程序员,除了 JVM、并发编程等基础知识,Spring 必然是另一个绕不开的主题。Spring 框架事实上已经成为了各大公司使用 Java 进行开发时的首选,市面上各种技术层出不穷,但 Spring 全家桶却越来越全,历久弥新。

微服务架构目前大行其道,使用 SpringBoot、Spring Cloud 进行构建也更加流行。可究其本质,Spring 框架还是全家桶所有新奇技术的基础,其中 IOC 和 AOP 又是它的两大灵魂。

本文将对 AOP 的思想和实现从以下几个方面来讲述,相信大家耐心看了之后肯定有收获,码字不易,别忘了「在看」,「转发」哦。

  • AOP 的前生今世

  • 代理模式

  • JDK 动态代理

  • CGLIB 动态代理

  • 自定义注解实现 AOP

正文

01 AOP 的前生今世

AOP 是什么?

传统的 OOP 开发过程中,代码的逻辑是自上而下的。在这些自上而下的过程中会产生一些横切性的问题,而这些横切性的问题往往与业务逻辑关系并不大,散落在代码的各个地方,造成难以维护。

举个例子,对于日志功能,它的代码往往水平地散布在所有对象的层次中,而往往与它所散布到的对象核心功能毫无关系。对于其他类型的代码,如安全性、异常处理和透明的持续性也是如此。

原始代码:

public void foo() {//do something...}

当需要在完成业务逻辑的同时记录日志,传统的做法:

  public void foo() {//do something...writeLog(); //执行日志记录}   

这种散布在各处的无关的代码被称为横切(cross-cutting)代码,在 OOP 设计中,它导致了大量代码的重复,导致不利于各个模块重用。

AOP 编程思想就是把业务和横切问题进行分离,从而达到解耦的目的,使代码的重用性和开发效率更高。

AOP 的实现主要基于代理思想,对原来的目标对象,创建代理对象。在不修改原对象代码情况下,通过代理对象调用增强功能的代码,从而对原有业务方法进行增强。

AOP 的应用场景非常多,比如:

  • 日志记录

  • 权限校验、控制

  • 效率检查(记录执行时间…)

  • 事务管理(调用方法前开启事务,调用方法后提交关闭事务)

  • 错误、异常处理

  • 内容传递、增强

02 代理模式

代理模式的基本思想是给目标对象提供一个代理对象,并由代理对象控制对目前对象的引用,这样的好处有两个:

(1)通过代理对象来间接访问目标,防止了直接访问给系统带来的复杂性。

(2)实现了对原有业务的增强。

为了保持行为的一致性,代理类和委托类通常会实现相同的接口,所以在访问者看来两者没有丝毫的区别。

通过代理类这中间一层,能有效控制对委托类对象的直接访问,也可以很好地隐藏和保护委托类对象,同时也为实施不同控制策略预留了空间,从而在设计上获得了更大的灵活性。

更通俗的说,代理解决的问题当两个类需要通信时,引入第三方代理类,将两个类的关系解耦,让我们只了解代理类即可。

而且代理的出现还可以让我们完成与另一个类之间的关系的统一管理,但是切记,代理类和委托类要实现相同的接口,因为代理真正调用的还是委托类的方法。

在介绍动态代理前,我们先来看一下静态代理的方式。

静态代理在使用时,需要定义接口或者父类,被代理对象与代理对象一起实现相同的接口或者继承的类。

静态代理是由程序员创建代理类或特定工具自动生成源代码再对其编译,在程序运行前代理类的.class文件就已经存在了。

举个例子,添加打印日志的功能,即每个方法调用之前和调用之后写入日志。

用户管理实现类.java

public class UserManagerImpl implements UserManager {...@Overridepublic String findUser(String userId) {return "张三";}...
}

用户管理实现代理类.java

public class UserManagerImplProxy implements UserManager {// 目标对象private UserManager userManager;// 通过构造方法传入目标对象public UserManagerImplProxy(UserManager userManager){this.userManager=userManager;}@Overridepublic void findUser(String userId) {// 添加打印日志的功能System.out.println("start-->findUser()");// 开始查询用户return userManager.findUser(userId);}
}

显而易见,静态代理存在以下几个缺点:

1、代理类和委托类必须实现相同接口,并且代理类通过委托类实现了相同的方法,这样就出现了大量的代码重复。

2、如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。

3、代理对象只服务于一种类型的对象,如果要服务多类型的对象。势必要为每一种对象都进行代理,静态代理在程序规模稍大时就无法胜任了。比如上面的代码只为 UserManager 类的访问提供了代理,但如果还要为如 DepartmentManager 类提供代理的话,就需要我们再次添加代理 DepartmentManager 的代理类。

03 JDK动态代理

由于静态代理存在的诸多不便,自然我们就会想到引入动态代理。

动态代理是生成一个包装类对象,由于代理的对象是动态的,所以叫动态代理。代理的主要目的是为了进行增强操作,这个增强是需要留给开发人员开发代码的。

因此代理类不能直接包含被代理对象,而是一个 InvocationHandler,该 InvocationHandler 包含被代理对象,并负责分发请求给被代理对象,分发前后均可以做增强。从原理可以看出,JDK 动态代理是“对象”的代理。

在上面的静态代理示例中,一个代理只能代理一种类型,而且是在编译器就已经确定被代理的对象。而动态代理是在运行时,通过反射机制实现动态代理,并且能够代理各种类型的对象。

在 Java 中要想实现动态代理机制,需要 java.lang.reflect.InvocationHandler接口和 java.lang.reflect.Proxy 类的支持

java.lang.reflect.InvocationHandler接口的定义如下:

//  Object proxy:被代理的对象
//  Method method:要调用的方法
//  Object[] args:方法调用时所需要参数public interface InvocationHandler {public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;}

java.lang.reflect.Proxy 类的定义如下:

//  CLassLoader loader:类的加载器
//  Class<?> interfaces:得到全部的接口
//  InvocationHandler h:得到InvocationHandler接口的子类的实例public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException

下面举例采用动态代理的方式,对用户管理实现类进行日志功能代理:

//动态代理类只能代理接口(不支持抽象类),代理类都需要实现InvocationHandler类,实现invoke方法。该invoke方法就是调用被代理接口的所有方法时需要调用的,该invoke方法返回的值是被代理接口的一个实现类

public class LogHandler implements InvocationHandler {// 目标对象private Object targetObject;//绑定关系,也就是关联到哪个接口(与具体的实现类绑定)的哪些方法将被调用时,执行invoke方法。            public Object newProxyInstance(Object targetObject){this.targetObject=targetObject;//该方法用于为指定类装载器、一组接口及调用处理器生成动态代理类实例  //第一个参数指定产生代理对象的类加载器,需要将其指定为和目标对象同一个类加载器//第二个参数要实现和目标对象一样的接口,所以只需要拿到目标对象的实现接口//第三个参数表明这些被拦截的方法在被拦截时需要执行哪个InvocationHandler的invoke方法//根据传入的目标返回一个代理对象return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(),targetObject.getClass().getInterfaces(),this);}@Override//关联的这个实现类的方法被调用时将被执行/*InvocationHandler接口的方法,proxy表示代理,method表示原对象被调用的方法,args表示方法的参数*/public Object invoke(Object proxy, Method method, Object[] args)throws Throwable {Object ret=null;try{/*原对象方法调用前处理日志信息*/System.out.println("satrt-->>");//调用目标方法ret=method.invoke(targetObject, args);/*原对象方法调用后处理日志信息*/System.out.println("success-->>");}catch(Exception e){e.printStackTrace();System.out.println("error-->>");throw e;}return ret;}
}

客户端代码:

public class Client {public static void main(String[] args){LogHandler logHandler=new LogHandler();UserManager userManager=(UserManager)logHandler.newProxyInstance(new UserManagerImpl());userManager.findUser("1111");}
}

由以上例子可以看到,我们可以通过 LogHandler 代理不同类型的对象,如果我们把对外的接口都通过动态代理来实现,那么所有的函数调用最终都会经过invoke 函数的转发。

因此我们就可以在这里做一些自己想做的操作,比如日志系统、事务、拦截器、权限控制等。这也就是 AOP 的基本原理。

04 CGLIB动态代理

CGLIB(Code Generator Library)是一个强大的、高性能的代码生成库,可以在运行期间扩展 Java 类与实现 Java 接口。

其被广泛应用于 AOP 框架中,用以提供方法拦截操作。Hibernate 作为一个受欢迎的 ORM 框架,同样使用CGLIB 来代理单端(多对一和一对一)关联(延迟提取集合使用的另一种机制)。

为什么使用CGLIB

CGLIB 代理主要通过对字节码的操作,为对象引入间接级别,以控制对象的访问。我们知道 Java 中的动态代理也是做这个事情的,那我们为什么不直接使用Java 动态代理,而要使用 CGLIB 呢?

答案是 CGLIB 相比于 JDK 动态代理更加强大,JDK 动态代理虽然简单易用,但是其有一个致命缺陷是,只能对接口进行代理。如果要代理的类为一个普通类、没有接口,那么 Java 动态代理就没法使用了。

而 CGLIB 不仅可以接管接口类的方法,也可以接管普通类的方法,为 JDK 的动态代理提供了很好的补充。

CGLIB 底层使用了 Java 字节码操作框架 ASM。它是一个短小精悍的字节码操作框架,用于操作字节码生成新的类。除了 CGLIB 库外,脚本语言如 Groovy也使用 ASM 生成字节码。ASM 使用类似 SAX 的解析器来实现高性能。我们不鼓励直接使用 ASM,因为它需要对 Java 字节码的格式足够的了解。

CGLIB的原理

CGLIB 底层采用 ASM 字节码生成框架,使用字节码技术生成代理类。

CGLIB 是动态生成被代理类的子类,子类重写委托类的所有非 private、非 final 的方法。在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。

因此如果委托类被 final 修饰,那么它就不可以被继承,导致不可以被代理。同理如果委托类的一个方法被 final 修饰后,那么此方法也不可以被代理。

下面举例使用 CGLIB 完成日志记录:

public class LogCGlibProxy implements MethodInterceptor {public Object newProxyInstance(Class clazz) {// 创建Enhancer对象,类似于JDK动态代理的Proxy类,下一步就是设置几个参数Enhancer enhancer = new Enhancer();// 设置目标类的字节码文件enhancer.setSuperclass(clazz);// 设置回调函数enhancer.setCallback(this);return enhancer.create();}@Overridepublic Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {System.out.println("调用代理对象前");Object result = methodProxy.invokeSuper(proxy, args);System.out.println("调用代理对象后");return result;}}

在 Spring 中 AOP 的实现方式遵循以下原则:

(1)如果目标对象实现了接口,默认采用 JDK 动态代理进行实现。

(2)如果目标对象实现了接口,也可以强制用 CGLIB 进行实现。

(3)如果目前对象没有接口,则必须采用 CGLIB 实现动态代理。

05 自定义注解实现AOP

AOP 是一种概念,Spring AOP 与 AspectJ 都是AOP的实现方式。Spring AOP 有自己的语法,但是较为复杂。因此 Spring AOP 借鉴了 AspectJ 的语法格式(注解),但是底层还有由自己本身实现,也就是 JDK 动态代理和 CGLIB 动态代理。

@Aspect 利用AspectJ注解语法
xml aop:config 利用Spring命名空间

Java 注解是 JDK5.0 版本开始支持加入源代码的特殊语法元数据。

Java 语言中的类、方法、变量、参数和包等都可以被标注。和 Javadoc 不同,Java 标注可以通过反射获取标注内容。

在编译器生成类文件时,标注可以被嵌入到字节码中。Java 虚拟机可以保留标注内容,在运行时可以获取到标注内容。当然它也支持自定义 Java 标注。

元注解

Target:描述了注解修饰的对象范围,取值在java.lang.annotation.ElementType 定义,常用的包括:

METHOD:用于描述方法

PACKAGE:用于描述包

PARAMETER:用于描述方法变量

TYPE:用于描述类、接口或enum类型

Retention: 表示注解保留时间长短。取值在 java.lang.annotation.RetentionPolicy 中,取值为:

SOURCE:在源文件中有效,编译过程中会被忽略

CLASS:随源文件一起编译在class文件中,运行时忽略

RUNTIME:在运行时有效

只有定义为 RetentionPolicy.RUNTIME 时,我们才能通过注解反射获取到注解。

自定义注解

以权限校验的业务场景为例,在对资源进行操作时,需要先判断此用户是否有相对应的权限。

(1)自定义注解 @PermissionAuth,它有一个属性 role ,代表只有拥有声明的指定权限才可以进行资源操作。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PermissionAuth {String role();
}

(2)声明切面,对自定义注解 @PermissionAuth 拦截,定义前置权限校验业务。

@Component
@Aspect
public class PermissionAuthAspect {@Pointcut(value = "@annotation(com.xuwuji.spring.aop.PermissionAuth)")public void pointCut() {}/*** Validate User Permission** @param jwtAuth* @throws QmtException*/@Before(value = "pointCut()&&@annotation(permissionAuth)")public void validateRole(PermissionAuth permissionAuth) {// perimission check}}

(3)在用户访问资源时,如果资源需要权限校验,则在对应方法上添加自定义注解 @PermissionAuth

    @PermissionAuth(role = "admin”) //代表拥有admin权限的用户才能进行findUser的操作public User findUser(String userId) {return new User(userId, map.get(userId));

Offer收割机》系列持续更新,也会定期分享互联网常用技术栈相关的文章,GitHub 上已经收录 https://github.com/BeKingCoding/JavaKing ,讲解一线大厂面试要求的核心知识点、并有对标阿里P7级别的成长体系脑图,欢迎加入技术交流群,我们一起有点东西。


在这里插入图片描述


我是一言不合就开车的代码界老司机无忌。创作不易,各位的支持和认可,就是我创作的最大动力,我们下篇文章见!
在这里插入图片描述

这篇关于一文带你看清 AOP 所有概念!的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/627433

相关文章

一文带你搞懂Nginx中的配置文件

《一文带你搞懂Nginx中的配置文件》Nginx(发音为“engine-x”)是一款高性能的Web服务器、反向代理服务器和负载均衡器,广泛应用于全球各类网站和应用中,下面就跟随小编一起来了解下如何... 目录摘要一、Nginx 配置文件结构概述二、全局配置(Global Configuration)1. w

【VUE】跨域问题的概念,以及解决方法。

目录 1.跨域概念 2.解决方法 2.1 配置网络请求代理 2.2 使用@CrossOrigin 注解 2.3 通过配置文件实现跨域 2.4 添加 CorsWebFilter 来解决跨域问题 1.跨域概念 跨域问题是由于浏览器实施了同源策略,该策略要求请求的域名、协议和端口必须与提供资源的服务相同。如果不相同,则需要服务器显式地允许这种跨域请求。一般在springbo

【MRI基础】TR 和 TE 时间概念

重复时间 (TR) 磁共振成像 (MRI) 中的 TR(重复时间,repetition time)是施加于同一切片的连续脉冲序列之间的时间间隔。具体而言,TR 是施加一个 RF(射频)脉冲与施加下一个 RF 脉冲之间的持续时间。TR 以毫秒 (ms) 为单位,主要控制后续脉冲之前的纵向弛豫程度(T1 弛豫),使其成为显著影响 MRI 中的图像对比度和信号特性的重要参数。 回声时间 (TE)

计算机网络基础概念 交换机、路由器、网关、TBOX

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、VLAN是什么?二 、交换机三、路由器四、网关五、TBOXTelematics BOX,简称车载T-BOX,车联网系统包含四部分,主机、车载T-BOX、手机APP及后台系统。主机主要用于车内的影音娱乐,以及车辆信息显示;车载T-BOX主要用于和后台系统/手机APP通信,实现手机APP的车辆信息显示与控

01 Docker概念和部署

目录 1.1 Docker 概述 1.1.1 Docker 的优势 1.1.2 镜像 1.1.3 容器 1.1.4 仓库 1.2 安装 Docker 1.2.1 配置和安装依赖环境 1.3镜像操作 1.3.1 搜索镜像 1.3.2 获取镜像 1.3.3 查看镜像 1.3.4 给镜像重命名 1.3.5 存储,载入镜像和删除镜像 1.4 Doecker容器操作 1.4

【机器学习-一-基础概念篇】

机器学习 定义分类算法 应用 定义 机器学习最早是被Arthur Samuel 提出的一个概念,指计算机无需明确编程即可学习的研究领域。1950年他发明的跳棋程序,这个人机对弈游戏让他的声名鹊起,机器学习这个概念才进入大众的是视线。 在这个跳棋程序里,他编程了一种算法,这个程序与Arthur下了数万次跳棋,计算机逐渐学会了下在哪里有更大的可能会赢得比赛,哪里会输,通过这种方法,最

SSM项目使用AOP技术进行日志记录

本步骤只记录完成切面所需的必要代码 本人开发中遇到的问题: 切面一直切不进去,最后发现需要在springMVC的核心配置文件中中开启注解驱动才可以,只在spring的核心配置文件中开启是不会在web项目中生效的。 之后按照下面的代码进行配置,然后前端在访问controller层中的路径时即可观察到日志已经被正常记录到数据库,代码中有部分注释,看不懂的可以参照注释。接下来进入正题 1、导入m

【吊打面试官系列-Redis面试题】说说 Redis 哈希槽的概念?

大家好,我是锋哥。今天分享关于 【说说 Redis 哈希槽的概念?】面试题,希望对大家有帮助; 说说 Redis 哈希槽的概念? Redis 集群没有使用一致性 hash,而是引入了哈希槽的概念,Redis 集群有 16384 个哈希槽,每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽, 集群的每个节点负责一部分 hash 槽。

Collection的所有的方法演示

import java.util.ArrayList;import java.util.Collection;import java.util.Iterator;public class TestCollection {/*** @param args* Collection的所有的方法演示* 此程序没有使用泛型,所以可以添加任意类型* 以后如果写到泛型会补充这一方面的内容*/public s

Temu官方宣导务必将所有的点位材料进行检测-RSL资质检测

关于饰品类产品合规问题宣导: 产品法规RSL要求 RSL测试是根据REACH法规及附录17的要求进行测试。REACH法规是欧洲一项重要的法规,其中包含许多对化学物质进行限制的规定和高度关注物质。 为了确保珠宝首饰的安全性,欧盟REACH法规规定,珠宝首饰上架各大电商平台前必须进行RSLReport(欧盟禁限用化学物质检测报告)资质认证,以确保产品不含对人体有害的化学物质。 RSL-铅,