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

相关文章

使用Navicat工具比对两个数据库所有表结构的差异案例详解

《使用Navicat工具比对两个数据库所有表结构的差异案例详解》:本文主要介绍如何使用Navicat工具对比两个数据库test_old和test_new,并生成相应的DDLSQL语句,以便将te... 目录概要案例一、如图两个数据库test_old和test_new进行比较:二、开始比较总结概要公司存在多

一文详解Java Condition的await和signal等待通知机制

《一文详解JavaCondition的await和signal等待通知机制》这篇文章主要为大家详细介绍了JavaCondition的await和signal等待通知机制的相关知识,文中的示例代码讲... 目录1. Condition的核心方法2. 使用场景与优势3. 使用流程与规范基本模板生产者-消费者示例

电脑密码怎么设置? 一文读懂电脑密码的详细指南

《电脑密码怎么设置?一文读懂电脑密码的详细指南》为了保护个人隐私和数据安全,设置电脑密码显得尤为重要,那么,如何在电脑上设置密码呢?详细请看下文介绍... 设置电脑密码是保护个人隐私、数据安全以及系统安全的重要措施,下面以Windows 11系统为例,跟大家分享一下设置电脑密码的具体办php法。Windo

一文详解Python中数据清洗与处理的常用方法

《一文详解Python中数据清洗与处理的常用方法》在数据处理与分析过程中,缺失值、重复值、异常值等问题是常见的挑战,本文总结了多种数据清洗与处理方法,文中的示例代码简洁易懂,有需要的小伙伴可以参考下... 目录缺失值处理重复值处理异常值处理数据类型转换文本清洗数据分组统计数据分箱数据标准化在数据处理与分析过

SpringBoot实现动态插拔的AOP的完整案例

《SpringBoot实现动态插拔的AOP的完整案例》在现代软件开发中,面向切面编程(AOP)是一种非常重要的技术,能够有效实现日志记录、安全控制、性能监控等横切关注点的分离,在传统的AOP实现中,切... 目录引言一、AOP 概述1.1 什么是 AOP1.2 AOP 的典型应用场景1.3 为什么需要动态插

在MyBatis的XML映射文件中<trim>元素所有场景下的完整使用示例代码

《在MyBatis的XML映射文件中<trim>元素所有场景下的完整使用示例代码》在MyBatis的XML映射文件中,trim元素用于动态添加SQL语句的一部分,处理前缀、后缀及多余的逗号或连接符,示... 在MyBATis的XML映射文件中,<trim>元素用于动态地添加SQL语句的一部分,例如SET或W

C#实现获得某个枚举的所有名称

《C#实现获得某个枚举的所有名称》这篇文章主要为大家详细介绍了C#如何实现获得某个枚举的所有名称,文中的示例代码讲解详细,具有一定的借鉴价值,有需要的小伙伴可以参考一下... C#中获得某个枚举的所有名称using System;using System.Collections.Generic;usi

一文带你理解Python中import机制与importlib的妙用

《一文带你理解Python中import机制与importlib的妙用》在Python编程的世界里,import语句是开发者最常用的工具之一,它就像一把钥匙,打开了通往各种功能和库的大门,下面就跟随小... 目录一、python import机制概述1.1 import语句的基本用法1.2 模块缓存机制1.

通过C#获取PDF中指定文本或所有文本的字体信息

《通过C#获取PDF中指定文本或所有文本的字体信息》在设计和出版行业中,字体的选择和使用对最终作品的质量有着重要影响,然而,有时我们可能会遇到包含未知字体的PDF文件,这使得我们无法准确地复制或修改文... 目录引言C# 获取PDF中指定文本的字体信息C# 获取PDF文档中用到的所有字体信息引言在设计和出

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

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