12AOP面向切面编程/GoF之代理模式

先看一个例子:

声明一个接口:

java 复制代码
//       + - * / 运算的标准接口!
public interface Calculator {
    
    int add(int i, int j);
    
    int sub(int i, int j);
    
    int mul(int i, int j);
    
    int div(int i, int j);  
}

实现该接口:

java 复制代码
package com.sunsplanter.proxy;

/**
 * 在每个方法中,输出传入的参数和计算后的返回结果!
 */
public class CalculatorLogImpl implements Calculator {
    
    @Override
    public int add(int i, int j) {
    
        System.out.println("参数是:" + i + "," + j);
        int result = i + j;
        System.out.println("方法内部 result = " + result);
      
        return result;
    }
    
    @Override
    public int sub(int i, int j) {
    
        System.out.println("参数是:" + i + "," + j);
    
        int result = i - j;
    
        System.out.println("方法内部 result = " + result);
        return result;
    }
    
    @Override
    public int mul(int i, int j) {
    
        System.out.println("参数是:" + i + "," + j);
    
        int result = i * j;
    
        System.out.println("方法内部 result = " + result);
    
        return result;
    }
    
    @Override
    public int div(int i, int j) {
    
        System.out.println("参数是:" + i + "," + j);
    
        int result = i / j;
    
        System.out.println("方法内部 result = " + result);
        
        return result;
    }
}

存在的问题:

  1. 核心功能与边缘功能掺杂(即计算的过程和sout掺杂)
  2. 边缘功能尽管重复,但却大量分散,不利于管理

目标: 将重复的代码统一提取,并且[[动态插入]]到每个业务方法!

用代理模式解决:

相关术语:

  • 代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法。(中介)
    • 动词:指做代理这个动作,或这项工作
    • 名词:扮演代理这个角色的类、对象、方法
  • 目标:被代理"套用"了核心逻辑代码的类、对象、方法。(房东)

解决问题的思维:AOP

解决问题技术:代理技术

代理技术太麻烦,因此使用框架

Spring AOP框架(底层是代理技术:jdk动态daili,cglib)


代理在开发中实现的方式具体有两种:静态代理,[动态代理技术]

1. 静态代理解决了功能掺杂

缺点:静态代理确实实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性。就拿sout来说,将来其他地方也需要sout,那还得再声明更多个静态代理类,那就产生了大量重复的代码,sout还是分散的,没有统一管理。

项目结构:

原有代码不变,

使用静态代理即主动创建一个代理类CalculatorStaticProxy实现接口,:

java 复制代码
public class CalculatorStaticProxy implements Calculator {
    
    // 将被代理的目标对象声明为成员变量(令代理类与被代理对象建立关系)
    private Calculator target;
    //将代理对象传入构造方法,"注入"
    public CalculatorStaticProxy(Calculator target) {
        this.target = target;
    }
    
    @Override
    public int add(int i, int j) {
    
        // 附加功能由代理类中的代理方法来实现
        System.out.println("参数是:" + i + "," + j);
    
        // 通过目标对象来实现核心业务逻辑
        int addResult = target.add(i, j);
    
        System.out.println("方法内部 result = " + result);
    
        return addResult;
    }
        @Override
    public int sub(int i, int j) {
    
        // 附加功能由代理类中的代理方法来实现
        System.out.println("参数是:" + i + "," + j);
    
        // 通过目标对象来实现核心业务逻辑
        int subResult = target.sub(i, j);
    
        System.out.println("方法内部 result = " + result);
    
        return subResult;
    }
    ......

提出进一步的需求:将sout集中到一个代理类中,将来有任何日志需求,都通过这一个代理类来实现。这就需要使用动态代理技术了。

2. 动态代理进一步解决了集中管理边缘功能

动态代理技术分类

  • JDK动态代理:JDK原生的实现方式,需要被代理的目标类必须实现接口(也即 必须有接口)!他会根据目标类的接口动态生成一个代理对象!代理对象和目标对象有相同的接口!(接口作为父亲,代理对象和目标对象拜把子)
  • cglib:通过继承被代理的目标类实现代理,可以代理没有实现接口的类!(也即不需要有接口,代理对象认目标对象为干爹)

JDK动态代理技术实现(了解)

代理工程:基于jdk代理技术,生成代理对象

在这里我们不给出代理的具体实现代码,后续会学习Spring AOP框架,该框架封装动态代理技术,们只需写少量的配置,指定生效范围即可,即可完成面向切面思维编程的实现!


3. AOP思维

  • OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。OOP允许开发者定义纵向的关系。但在OOP设计中,它导致了大量公共代码的重复,而不利于各个模块的重用。

  • AOP技术恰恰相反,它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。

  • 使用AOP,可以在不修改原来代码的基础上添加新功能。

  • AOP思想主要的应用场景

    1. 日志记录:在系统中记录日志是非常重要的,可以使用AOP来实现日志记录的功能,可以在方法执行前、执行后或异常抛出时记录日志。
    2. 事务处理:在数据库操作中使用事务可以保证数据的一致性,可以使用AOP来实现事务处理的功能,可以在方法开始前开启事务,在方法执行完毕后提交或回滚事务。
    3. 安全控制:在系统中包含某些需要安全控制的操作,如登录、修改密码、授权等,可以使用AOP来实现安全控制的功能。可以在方法执行前进行权限判断,如果用户没有权限,则抛出异常或转向到错误页面,以防止未经授权的访问。
    4. 性能监控:在系统运行过程中,有时需要对某些方法的性能进行监控,以找到系统的瓶颈并进行优化。可以使用AOP来实现性能监控的功能,可以在方法执行前记录时间戳,在方法执行完毕后计算方法执行时间并输出到日志中。
    5. 异常处理:系统中可能出现各种异常情况,如空指针异常、数据库连接异常等,可以使用AOP来实现异常处理的功能,在方法执行过程中,如果出现异常,则进行异常处理(如记录日志、发送邮件等)。
    6. 缓存控制:在系统中有些数据可以缓存起来以提高访问速度,可以使用AOP来实现缓存控制的功能,可以在方法执行前查询缓存中是否有数据,如果有则返回,否则执行方法并将方法返回值存入缓存中。
    7. 动态代理:AOP的实现方式之一是通过动态代理,可以代理某个类的所有方法,用于实现各种功能。
  • AOP术语名词介绍

    1-横切关注点

    从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。

    有十个附加功能,就有十个横切关注点。

    AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事务、异常等。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。

    2-通知(增强)

    每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。就是具体你要织入的代码。

    • 前置通知:在被代理的目标方法前执行
    • 返回通知:在被代理的目标方法成功结束后执行(寿终正寝
    • 异常通知:在被代理的目标方法异常结束后执行(死于非命
    • 后置通知:在被代理的目标方法最终结束后执行(盖棺定论
    • 环绕通知(最常用):使用try...catch...finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置。

    3-切入点 pointcut

    定位连接点的方式,或者可以理解成被选中的连接点!

    是一个表达式,比如execution(* com.spring.service.impl..(...))。符合条件的每个方法都是一个具体的连接点。

    4-切面 aspect

    切入点和通知的结合。是一个类。

    3-连接点 joinpoint

    这也是一个纯逻辑概念,不是语法定义的。

    指那些被拦截到的点。在 Spring 中,可以被动态代理拦截目标类的方法

    6-目标 target

    被代理的目标对象。

    7-代理 proxy

    向目标对象应用通知之后创建的代理对象。

    8-织入 weave

    指把通知应用到目标上,生成代理对象的过程。可以在编译期织入,也可以在运行期织入,Spring采用后者。

3.1 AOP封装层次

3.2 注解实现AOP

3.2.1 切点表达式:定义通知(Advice)往哪些方法上切入。

切入点表达式语法格式:

execution([访问控制权限修饰符] 返回值类型 [全限定类名]方法名(形式参数列表) [异常])

访问控制权限修饰符:

  • 可选项。没写,就是4个权限都包括。
  • 写public就表示只包括公开的方法。

返回值类型:

  • 必填项。
  • *表示返回值类型任意。

全限定类名:

  • 可选项。
  • 两个点"..."代表当前包以及子包下的所有类。
  • 省略时表示所有的类。

方法名:

  • 必填项。

  • *表示所有方法。

  • set*表示所有的set方法。

    r
    形式参数列表:

  • 必填项

  • () 表示没有参数的方法

  • (...) 参数类型和个数随意的方法

  • (*) 只有一个参数的方法

  • (*, String) 第一个参数类型随意,第二个参数是String的。

异常:

  • 可选项。
  • 省略时表示任意异常类型。

3.2.2 简单演示AOP的实现

  1. 首先是导入依赖,实现AOP需要导入Spring-aspect,但Spring-aop包括了Spring-aspect,Spring-context又包括了Spring-aop,因此之前导入的Spring-context就够了。
  2. 在spring配置文件中增加命名空间; 启用自动代理
xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
                           
    <!--开启组件扫描-->
    <context:component-scan base-package="com.powernode.spring6.service"/>
    <!--开启自动代理后,凡是带有@Aspect注解的bean都会生成代理对象。
    proxy-target-class是可选属性,默认为false时采用JDK动态代理,
    但即便是最坏的情况,即采用JDK动态代理,而类又没有实现接口,Spring仍会自动转向cglib动态代理-->
    <aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>
</beans>
  1. 定义目标类以及目标方法, 纳入Spring管理
java 复制代码
package com.sunsplanter.spring6.service;

// 目标类
@Component
public class OrderService {
    // 目标方法
    public void generate(){
        System.out.println("订单已生成!");
    }
}

第二步:定义切面类, 纳入Spring管理

java 复制代码
package com.sunsplanter.spring6.service;

import org.aspectj.lang.annotation.Aspect;

// 切面类的注解
@Aspect
@Component
public class MyAspect {
}

第三步:在Spring配置文件中添加组建扫描

cpp 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
    <!--开启组件扫描-->
    <context:component-scan base-package="com.powernode.spring6.service"/>
</beans>

第四步: 在切面类中定义一个切面

java 复制代码
package com.sunsplanter.spring6.service;

import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import org.aspectj.lang.annotation.Aspect;

// 切面类
@Aspect
@Component
public class MyAspect {
    //通知+切点=切面
    // 切点表达式,@Before即前置通知
    @Before("execution(* com.powernode.spring6.service.OrderService.*(..))")
    // 这就是需要增强的代码(通知)
    public void advice(){
        System.out.println("我是一个通知");
    }
}

第五步:测试程序:

java 复制代码
package com.sunsplanter.spring6.test;

import com.sunsplanter.spring6.service.OrderService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class AOPTest {
    @Test
    public void testAOP(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-aspectj-aop-annotation.xml");
        OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
        orderService.generate();
    }
}

目标:横向插入增强代码(日志输出/)

相关推荐
Exclusive_Cat2 小时前
SpringMVC参数接收与数据返回详解
spring·mvc
ChinaRainbowSea3 小时前
补充:问题:CORS ,前后端访问跨域问题
java·spring boot·后端·spring
hqxstudying5 小时前
java依赖注入方法
java·spring·log4j·ioc·依赖
春生野草6 小时前
关于SpringMVC的整理
spring
Bug退退退1237 小时前
RabbitMQ 高级特性之重试机制
java·分布式·spring·rabbitmq
hello早上好8 小时前
CGLIB代理核心原理
java·spring
先睡14 小时前
Redis的缓存击穿和缓存雪崩
redis·spring·缓存
Bug退退退12318 小时前
RabbitMQ 高级特性之死信队列
java·分布式·spring·rabbitmq
booooooty1 天前
基于Spring AI Alibaba的多智能体RAG应用
java·人工智能·spring·多智能体·rag·spring ai·ai alibaba
极光雨雨1 天前
Spring Bean 控制销毁顺序的方法总结
java·spring