一文带你看清 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

相关文章

一文详解Java异常处理你都了解哪些知识

《一文详解Java异常处理你都了解哪些知识》:本文主要介绍Java异常处理的相关资料,包括异常的分类、捕获和处理异常的语法、常见的异常类型以及自定义异常的实现,文中通过代码介绍的非常详细,需要的朋... 目录前言一、什么是异常二、异常的分类2.1 受检异常2.2 非受检异常三、异常处理的语法3.1 try-

一文带你搞懂Python中__init__.py到底是什么

《一文带你搞懂Python中__init__.py到底是什么》朋友们,今天我们来聊聊Python里一个低调却至关重要的文件——__init__.py,有些人可能听说过它是“包的标志”,也有人觉得它“没... 目录先搞懂 python 模块(module)Python 包(package)是啥?那么 __in

一文详解如何在Python中从字符串中提取部分内容

《一文详解如何在Python中从字符串中提取部分内容》:本文主要介绍如何在Python中从字符串中提取部分内容的相关资料,包括使用正则表达式、Pyparsing库、AST(抽象语法树)、字符串操作... 目录前言解决方案方法一:使用正则表达式方法二:使用 Pyparsing方法三:使用 AST方法四:使用字

电脑死机无反应怎么强制重启? 一文读懂方法及注意事项

《电脑死机无反应怎么强制重启?一文读懂方法及注意事项》在日常使用电脑的过程中,我们难免会遇到电脑无法正常启动的情况,本文将详细介绍几种常见的电脑强制开机方法,并探讨在强制开机后应注意的事项,以及如何... 在日常生活和工作中,我们经常会遇到电脑突然无反应的情况,这时候强制重启就成了解决问题的“救命稻草”。那

MySQL中动态生成SQL语句去掉所有字段的空格的操作方法

《MySQL中动态生成SQL语句去掉所有字段的空格的操作方法》在数据库管理过程中,我们常常会遇到需要对表中字段进行清洗和整理的情况,本文将详细介绍如何在MySQL中动态生成SQL语句来去掉所有字段的空... 目录在mysql中动态生成SQL语句去掉所有字段的空格准备工作原理分析动态生成SQL语句在MySQL

Python 迭代器和生成器概念及场景分析

《Python迭代器和生成器概念及场景分析》yield是Python中实现惰性计算和协程的核心工具,结合send()、throw()、close()等方法,能够构建高效、灵活的数据流和控制流模型,这... 目录迭代器的介绍自定义迭代器省略的迭代器生产器的介绍yield的普通用法yield的高级用法yidle

一文详解JavaScript中的fetch方法

《一文详解JavaScript中的fetch方法》fetch函数是一个用于在JavaScript中执行HTTP请求的现代API,它提供了一种更简洁、更强大的方式来处理网络请求,:本文主要介绍Jav... 目录前言什么是 fetch 方法基本语法简单的 GET 请求示例代码解释发送 POST 请求示例代码解释

一文详解SpringBoot响应压缩功能的配置与优化

《一文详解SpringBoot响应压缩功能的配置与优化》SpringBoot的响应压缩功能基于智能协商机制,需同时满足很多条件,本文主要为大家详细介绍了SpringBoot响应压缩功能的配置与优化,需... 目录一、核心工作机制1.1 自动协商触发条件1.2 压缩处理流程二、配置方案详解2.1 基础YAML

一文详解如何从零构建Spring Boot Starter并实现整合

《一文详解如何从零构建SpringBootStarter并实现整合》SpringBoot是一个开源的Java基础框架,用于创建独立、生产级的基于Spring框架的应用程序,:本文主要介绍如何从... 目录一、Spring Boot Starter的核心价值二、Starter项目创建全流程2.1 项目初始化(

Python实现将MySQL中所有表的数据都导出为CSV文件并压缩

《Python实现将MySQL中所有表的数据都导出为CSV文件并压缩》这篇文章主要为大家详细介绍了如何使用Python将MySQL数据库中所有表的数据都导出为CSV文件到一个目录,并压缩为zip文件到... python将mysql数据库中所有表的数据都导出为CSV文件到一个目录,并压缩为zip文件到另一个