面向切面的编程:AOP

前言#

AOP 全称 Aspect Oriented Programming 意为面向切面编程,也叫做面向方法编程,是通过预编译方式和运行期动态代理的方式实现不修改源代码的情况下给程序动态统一添加功能的技术

一段简单的解释#

面向对象的特点是继承、多态和封装。而封装就要求将功能分散到不同的对象中去,这在软件设计中往往称为职责分配。实际上也就是说,让不同的类设计不同的方法。这样代码就分散到一个个的类中去了。这样做的好处是降低了代码的复杂程度,使类可重用

但是人们也发现,在分散代码的同时,也增加了代码的重复性。什么意思呢?比如说,我们在两个类中,可能都需要在每个方法中做日志。按面向对象的设计方法,我们就必须在两个类的方法中都加入日志的内容。也许他们是完全相同的,但就是因为面向对象的设计让类与类之间无法联系,而不能将这些重复的代码统一起来

也许有人会说,那好办啊,我们可以将这段代码写在一个独立的类独立的方法里,然后再在这两个类中调用。但是,这样一来,这两个类跟我们上面提到的独立的类就有耦合了,它的改变会影响这两个类。那么,有没有什么办法,能让我们在需要的时候,随意地加入代码呢?这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程

在这个基础上,下面说几个 AOP 相关的基本概念

几个基本概念#

通知(Advice):希望抽取出的功能,例如日志处理、身份验证等

连接点(JoinPoint):就是 Spring 允许你使用通知的位置,一般是方法的前后和抛出异常时

切入点(Pointcut):上面说的连接点的基础上,来定义切入点,一个类里,有 15 个方法,就会有三十多个连接点,但并不想在所有方法附近都使用通知,只想让其中的几个,在调用这几个方法之前、之后或者抛出异常时干点什么,那么就用切点来定义这几个方法,让切点来筛选连接点,选中那几个你想要的方法

切面(Aspect):切面就是通知和切入点的合体,并没有连接点什么事,其实连接点就是为了使切入点更好理解而创造出来的一个概念

引入(introduction):允许向现有的类添加新方法属性。就是把切面(也就是新方法属性:通知定义的)用到目标类中

目标(target):引入中提到的目标类,就是要被通知的对象,即真正的业务逻辑

织入(weaving):把切面应用到目标对象来创建新的代理对象的过程

个人理解的 AOP#

以下内容摘自:Spring AOP概念理解 (通俗易懂)
Spring 用代理类包裹切面,把他们织入到 Spring 管理的 bean 中。也就是说代理类伪装成目标类,它会截取对目标类中方法的调用,让调用者对目标类的调用都先变成调用伪装类,伪装类中就先执行了切面,再把调用转发给真正的目标 bean
现在需要考虑的问题是:怎么搞出来这个伪装类,才不会被调用者发现(过 JVM 的检查,JAVA 是强类型检查,哪里都要检查类型)
1.实现和目标类相同的接口。我也实现和你一样的接口,反正上层都是接口级别的调用,这样我就伪装成了和目标类一样的类(实现了同一接口,咱是兄弟了),也就逃过了类型检查,到 Java 运行期的时候,利用多态的后期绑定(所以 Spring 采用运行时),伪装类(代理类)就变成了接口的真正实现,而他里面包裹了真实的那个目标类,最后实现具体功能的还是目标类,只不过伪装类在之前干了点事情(写日志,安全检查,事物等)
这就好比,一个人让你办件事,每次这个时候,你弟弟就会先出来,当然他分不出来了,以为是你,你这个弟弟虽然办不了这事,但是他知道你能办,所以就答应下来了,并且收了点礼物(写日志),收完礼物了,给把事给人家办了啊,所以你弟弟又找你这个哥哥来了,最后把这是办了的还是你自己。但是你自己并不知道你弟弟已经收礼物了,你只是专心把这件事情做好
顺着这个思路想,要是本身这个类就没实现一个接口呢,你怎么伪装我,我就压根没有机会让你搞出这个双胞胎的弟弟,那么就用第 2 种代理方式,创建一个目标类的子类,生个儿子,让儿子伪装我
2.生成子类调用,这次用子类来做为伪装类,当然这样也能逃过 JVM 的强类型检查,我继承的吗,当然查不出来了,子类重写了目标类的所有方法,当然在这些重写的方法中,不仅实现了目标类的功能,还在这些功能之前,实现了一些其他的(写日志,安全检查,事物等)
这次的对比就是,儿子先从爸爸那把本事都学会了,所有人都找儿子办事情,但是儿子每次办和爸爸同样的事之前,都要收点小礼物(写日志),然后才去办真正的事。当然爸爸是不知道儿子这么干的了。这里就有件事情要说,某些本事是爸爸独有的(final),儿子学不了,学不了就办不了这件事,办不了这个事情,自然就不能收人家礼了
前一种兄弟模式,Spring 会使用 JDK 的java.lang.reflect.Proxy类,它允许 Spring 动态生成一个新类来实现必要的接口,织入通知,并且把对这些接口的任何调用都转发到目标类。
后一种父子模式,Spring 使用 CGLIB 库生成目标类的一个子类,在创建这个子类的时候,Spring 织入通知,并且把对这个子类的调用委托到目标类。
相比之下,还是兄弟模式好些,他能更好的实现松耦合,尤其在今天都高喊着面向接口编程的情况下,父子模式只是在没有实现接口的时候,也能织入通知,应当做一种例外

AOP 代码实现#

一个简单的快速入门#

pom.xml

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>

配置类

@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class AspectConfig {
    
}

aspect类

@Component
@Aspect
public class LogAspect {
    @Before("execution(public * cn.lyy.demo.aspect.service.UserService.*(..))")
    public void doAccessCheck(){
        System.out.println("[before:AccessCheck]");
    }

    @Around("execution(public * cn.lyy.demo.aspect.service.AccountService.*(..))")
    public Object doLogging(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("[around:start]"+pjp.getSignature());
        Object retVal = pjp.proceed();
        System.out.println("[around:end]"+pjp.getSignature());
        return retVal;
    }
}

业务类

@Service
public class UserService {
    private void testPrivate(String msg) {
        System.out.println("User私有方法输出" + msg);
    }

    public void testPublic(String msg) {
        System.out.println("User公有方法输出" + msg);
    }
}
//------------------------------
@Service
public class AccountService {
    private void testPrivate(String msg) {
        System.out.println("Account私有方法输出" + msg);
    }

    public void testPublic(String msg) {
        System.out.println("Account公有方法输出" + msg);
    }
}

测试类

@SpringBootTest
public class ApectTest {
    @Resource
    private UserService userService;

    @Resource
    private AccountService accountService;

    @Test
    public void testMethod(){
        userService.testPublic("im user");
        accountService.testPublic("im account");
    }

}

输出结果

[before:AccessCheck]
User共有方法输出im user
[around:start]void cn.lyy.demo.aspect.service.AccountService.testPublic(String)
Account共有方法输出im account
[around:end]void cn.lyy.demo.aspect.service.AccountService.testPublic(String)

在 aspect 类中,存在三个注解:@Aspect@Before@Around

@Before后面的字符串是告诉 AspectJ 应该在何处执行该方法,这里写的意思是:执行 UserService 的每个 public 方法前执行 doAccessCheck() 代码

再观察 doLogging 方法,因为添加了@Around注解,所以在执行的时候先输出内容,再执行 AccountService 中指定方法,最后再输出内容

Spring 内部实现 AOP 的方式比较复杂,但是在使用的时候却不需要关注那么多的内容,只需要:

  • 定义执行方法,并在方法上通过 AspectJ 的注解告诉 Spring 应该在何处调用此方法
  • 标记@Component@Aspect
  • @Configuration类上标注@EnableAspectJAutoProxy

拦截器的类型#

@Before:这种拦截器先执行拦截代码,再执行目标代码。如果拦截器抛异常,那么目标代码就不执行了

@After:这种拦截器先执行目标代码,再执行拦截器代码。无论目标代码是否抛异常,拦截器代码都会执行

@AfterReturning:和@After不同的是,只有当目标代码正常返回时,才执行拦截器代码

@AfterThrowing:和@After不同的是,只有当目标代码抛出了异常时,才执行拦截器代码

@Around:能完全控制目标代码是否执行,并可以在执行前后、抛异常后执行任意拦截代码,可以说是包含了上面所有功能

使用注解装配 AOP#

在快速入门的例子中,通过一个复杂的表达式execution(public * cn.lyy.demo.aspect.service.UserService.*(..))来定义了前置通知作用的范围,然而这种方式在实际过程中应用的并不多。首先是它书写比较麻烦,需要指定的要素很多;其次,如果设置不恰当的范围,容易导致意想不到的结果,即很多不需要 AOP 代理的 Bean 也被自动代理了,并且,后续新增的 Bean,如果不清楚现有的 AOP 装配规则,容易被强迫装配

所以可以采用注解装配 AOP 实现精准控制的需求

注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogAnno {
    String value() default "";
}

Aspect类

@Component
@Aspect
public class TestApect {
    @Around("@annotation(logAnno)")
    public void testMethod(ProceedingJoinPoint joinpoint, LogAnno logAnno) throws Throwable {
        String value = logAnno.value();
        long start = System.currentTimeMillis();
        try {
            joinpoint.proceed();
        } finally {
            long time = System.currentTimeMillis() - start;
            System.out.println("time is:" + time);
        }
    }
}

业务类

@Service
public class UserService {
    @LogAnno(value = "register")
    public void testAnnotationAspect(String msg) {
        System.out.println("msg is:" + msg);
    }
}

测试类

@SpringBootTest
public class ApectTest {

    @Resource
    private UserService userService;

    @Test
    public void testMethod(){
        userService.testAnnotationAspect("hello");
    }

}
posted @   colee51666  阅读(67)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 周边上新:园子的第一款马克杯温暖上架
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?
· 使用C#创建一个MCP客户端
点击右上角即可分享
微信分享提示
主题色彩
哥伦布
15°
00:09发布
哥伦布
00:09发布
15°
中雨
西风
3级
空气质量
相对湿度
92%
今天
中雨
15°/22°
周一
中雨
6°/19°
周二
小雨
2°/10°