Spring AOP (面向切面编程)原理与代理模式—实例演示

news2024/12/28 2:32:48

 一、AOP介绍和应用场景

Spring 中文文档 (springdoc.cn)

Spring | Home 官网

1、AOP介绍(为什么会出现AOP ?)       

        Java是一个面向对象(OOP)的语言,但它有一些弊端。虽然使用OOP可以通过组合或继承的方式来实现代码的重用。但当我们需要为多个不具有继承关系的对象(一般指的是两个不同的类,它们之间没有继承自同一个父类或接口。它们无法通过继承来共享属性和方法。)引入一个公共行为,例如日志、权限验证、事务等功能时,同样的代码仍然会分散到各个方法中。如果想关闭某个功能或是对其进行修改,就必须修改所有的相关方法,这样做不便于维护,而且有大量重复代码。于是AOP的出现弥补了OOP的这点不足。

         AOP 是 Spring 框架提供的一个重要特性,与OOP(面向对象编程)不同的是,AOP主张将程序中相同的业务逻辑进行横向的隔离,将重复的业务逻辑抽取到一个独立的模块中,通过在运行时动态地将代码逻辑织入到目标对象的方法中,实现与业务逻辑解耦和重复代码的消除,提高了代码的可维护性和可扩展性。

2、AOP应用场景

这是官方文档中对应用场景进行的描述:

Authentication 权限 ;Caching 缓存 ; Context passing 内容传递;

Error handling 错误处理;Lazy loading 懒加载;Debugging 调试;

Logging 日志 ,tracing,profiling and monitoring 记录跟踪 优化 校准;

Performance optimization 性能优化 ;Persistence 持久化   ; Resource pooling 资源池

Synchronization 同步  ;Transactions 事务

二、AOP原理

 执行过程依次是

1、 在 Spring 中,创建 Bean 实例都是从 getBean()方法开始的, 在实例创建之后,Spring 容器将根据 AOP 的配置去匹配目标类的类名,看目标类的类名是否满足切面规则。如果满足满足切面规则,就会调用 ProxyFactory 创建代理 Bean 并缓存到 IoC 容器中。根据目标对象的自动选择不同的代理策略。如果目标类实现了接口,Spring 会默认选择 JDK Proxy,如果目标类没有实现接口,Spring 会默认选择 Cglib Proxy, 当然,我们也可以通过配置强制使用 Cglib Proxy

2、当用户调用目标对象的某个方法时,将会被一个叫做AopProxy 的对象拦截,Spring 将所有的调用策略封装到了这个对象中,它默认实现了 InvocationHandler 接口,也就是调用代理对象的外层拦截器。在这个接口的 invoke()方法中,会触发 MethodInvocation 的 proceed()方法。在这个方法中会按顺序执行符合所有 AOP 拦截规则的拦截器链。 

3、Spring AOP 拦截器链中的每个元素被命名为 MethodInterceptor,其实就是切面配置中的 Advice 通知。这个回调通知可以简单地理解为是新生成的代理 Bean 中的方法。也就是我们常说的被织入的代码片段,这些被织入的代码片段会在这个阶段执行。

4、MethodInterceptor 接口也有一个 invoke()方法,在 MethodInterceptor 的 invoke()方法中会触发对目标对象方法的调用,也就是反射调用目标对象的方法。

三、什么是代理模式

        当谈论代理模式时,可以通过两种常见例子进行解释:买火车票和Windows里的快捷方式。

  1. 买火车票:假设你需要买一张火车票,但你不想亲自去火车站排队购票。那么你可以找一个代理人,让他帮你完成购票的过程。代理人负责排队、选座位、支付等操作,并最终将车票交给你。在这个例子中,代理人扮演着你与火车站之间的中介角色,你通过代理人完成了购票过程,同时也避免了亲自去火车站排队的麻烦。

  2. Windows里的快捷方式:在Windows系统中,你可以使用桌面上的快捷方式来访问某个程序或文件。快捷方式实际上是一个代理,它引用了目标程序或文件的位置信息,并提供了一个方便的入口,使得你可以通过点击快捷方式来快速访问目标内容。快捷方式隐藏了底层的具体实现细节,简化了用户的操作过程。

下面是买火车票的示例代码:

// 创建一个抽象主题接口
interface Ticket {
    void purchase();
}

// 创建真实主题类,即需要购买火车票的对象
class TrainTicket implements Ticket {
    public void purchase() {
        System.out.println("购买火车票");
    }
}

// 创建代理类,即代理对象
class TicketProxy implements Ticket {
    private TrainTicket ticket;

    public void purchase() {
        if (ticket == null) {
            ticket = new TrainTicket();
        }
        // 通过代理对象调用真实对象的方法
        prePurchase();
        ticket.purchase();
        postPurchase();
    }

    private void prePurchase() {
        System.out.println("代理人处理排队、选座位等操作");
    }

    private void postPurchase() {
        System.out.println("代理人将车票交给顾客");
    }
}

// 客户端代码
public class ProxyPatternExample {
    public static void main(String[] args) {
        // 创建代理对象,并通过它来购买火车票
        Ticket ticketProxy = new TicketProxy();
        ticketProxy.purchase();
    }
}

        在上述示例中,Ticket是一个抽象主题接口定义了购票的方法。TrainTicket是真实主题类,代表了真正需要购票的对象。TicketProxy是代理类,实现了Ticket接口,它维护了一个对TrainTicket对象的引用,并在调用purchase方法前后进行了一些额外的操作,如排队和交票。通过创建代理对象并调用其purchase方法,我们可以间接地让代理对象完成购票过程,并在其中添加一些特定功能,而无需直接访问真实主题对象。

        而代理模式又分为两种,动态代理和静态代理。它们都是面向对象中的代理模式,作用都是为了在客户端与目标对象之间起到一个中介作用,拦截对目标对象的访问,以达到增强目标对象的功能或控制其访问权限的目的。

两者的区别如下:

        静态代理是在编译时期就已经确定的代理关系,代理类和目标类都是在编译时期就已经存在的。在静态代理中,需要手动创建代理类,代理类和目标类之间的关系是固定的。每当需要代理一个新的接口或者类时,都需要手动编写一个新的代理类。

        而动态代理是在运行时动态生成的代理类。通过 Java 提供的 java.lang.reflect.Proxy 类和 java.lang.reflect.InvocationHandler 接口,可以在运行时动态生成代理对象。动态代理不需要手动编写代理类,它在运行时根据接口信息动态地生成代理对象。使用动态代理时,代理类和目标类之间的关系是灵活的。只需要定义一个普通的接口,通过 InvocationHandler 来处理方法调用,可以代理多个不同的接口或类。

        动态代理的优点在于可以减少代码量,避免了编写大量重复的代理类。同时,它还可以实现更灵活的代理逻辑,可以在运行时动态地修改代理行为。

        总的来说,动态代理和静态代理都是代理模式的实现方式,静态代理通过手动编写代理类实现,而动态代理则通过反射机制在运行时动态生成代理类。

四、Spring AOP 的代理机制

        Spring 在运行期会为目标对象生成一个动态代理对象,并在代理对象中实现对目标对象的增强。Spring AOP 的底层是通过以下 2 种动态代理机制,为目标对象(Target Bean)执行横向织入的。

        在 Spring AOP 的配置中,可以通过 XML 或注解的方式声明切入点和切面。XML 方式需要配置切入点表达式、通知类型(如前置通知、后置通知等)和切面类的引用;注解方式则通过在切面类上添加注解来指定切入点和通知类型。

        Spring默认使用JDK动态代理,在需要代理类而不是代理接口的时候,Spring会自动切换为使用CGLIB代理,不过现在的项目都是面向接口编程,所以JDK动态代理相对来说用的还是多一些。

JDK代理和CGLIB代理有什么区别?

都不需要创建代理类,JDK 在运行时为我们动态的来创建,JDK代理是接口 。若目标类不存在接口,则使用Cglib生成代理,不管是JDK代理还是Cglib代理本质上都是对字节码进行操作。

代理技术描述
JDK 动态代理Spring AOP 默认的动态代理方式,若目标对象实现了若干接口,Spring 使用 JDK 的 java.lang.reflect.Proxy 类进行代理。
CGLIB 动态代理若目标对象没有实现任何接口,Spring 则使用 CGLIB 库生成目标对象的子类,以实现对目标对象的代理。

注意:由于被标记为 final 的方法是无法进行覆盖的,因此这类方法不管是通过 JDK 动态代理机制还是 CGLIB 动态代理机制都是无法完成代理的。

面试:让你实现一个JDK实现动态代理?你的思路是什么?

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

// 定义接口
interface Hello {
    void sayHello();
}

// 实现 InvocationHandler 接口
class MyInvocationHandler implements InvocationHandler {
    private Object target;

    public MyInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 在方法调用前后添加自定义逻辑
        System.out.println("Before method invocation");
        Object result = method.invoke(target, args);
        System.out.println("After method invocation");
        return result;
    }
}

public class DynamicProxyExample {
    public static void main(String[] args) {
        // 创建被代理对象
        Hello hello = new HelloImpl();

        // 创建 InvocationHandler 实例
        MyInvocationHandler handler = new MyInvocationHandler(hello);

        // 创建代理对象
        Hello proxyHello = (Hello) Proxy.newProxyInstance(
            hello.getClass().getClassLoader(),
            hello.getClass().getInterfaces(),
            handler
        );

        // 调用代理对象的方法
        proxyHello.sayHello();
    }
}

// 实现接口
class HelloImpl implements Hello {
    @Override
    public void sayHello() {
        System.out.println("Hello World!");
    }
}

如果使用CGLIB来实现动态代理要怎么实现呢 ?

引入 CGLIB 的依赖。在 Maven 项目中,可以添加以下依赖项到 pom.xml 文件中:
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>
创建一个普通的类作为目标类,该类不需要实现任何接口。例如:
public class TargetClass {
    public void doSomething() {
        System.out.println("Doing something...");
    }
}
创建一个实现了 MethodInterceptor 接口的类,该类负责处理方法的调用。在 intercept 方法中,可以编写逻辑来处理代理对象的操作。例如:
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class MyMethodInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("Before method invocation");
        Object result = proxy.invokeSuper(obj, args);
        System.out.println("After method invocation");
        return result;
    }
}
使用 CGLIB 创建代理对象。通过调用 Enhancer.create 方法,传入目标类的 Class 对象和 MethodInterceptor 对象,创建代理对象。例如:
import net.sf.cglib.proxy.Enhancer;

public class CglibDynamicProxyExample {
    public static void main(String[] args) {
        // 创建目标类的实例
        TargetClass target = new TargetClass();

        // 创建 MethodInterceptor 实例
        MyMethodInterceptor interceptor = new MyMethodInterceptor();

        // 使用 Enhancer 创建代理对象
        TargetClass proxy = (TargetClass) Enhancer.create(
            target.getClass(),
            interceptor
        );

        // 调用代理对象的方法
        proxy.doSomething();
    }
}
运行上述代码,会输出以下结果:
Before method invocation
Doing something...
After method invocation

CGLIB 可以在运行时生成目标类的子类,并通过拦截器拦截目标方法的调用。这样可以实现对目标方法的增强或拦截操作


五、实例—实现AOP对日志的记录 

步骤:

先要引入aop依赖

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aop</artifactId>
		</dependency>

然以根据以下三个步骤 

 


import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;

/**
 * 使用@Before在切入点开始处切入内容
 * 使用@After在切入点结尾处切入内容
 * 使用@AfterReturning在切入点return内容之后切入内容(可以用来对处理返回值做一些加工处理)
 * 使用@Around在切入点前后切入内容,并自己控制何时执行切入点自身的内容
 * 使用@AfterThrowing用来处理当切入内容部分抛出异常之后的处理逻辑
 */

@Aspect /**Description:  使之成为切面类*/
@Component /**Description: 把切面类加入到IOC容器中*/
public class AopLog {
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    //线程局部的变量,解决多线程中相同变量的访问冲突问题。
    ThreadLocal<Long> startTime = new ThreadLocal<>();

    //定义切点 1
    @Pointcut("execution(public * com.example..*.*(..))")
    public void aopWebLog() {
    }
    //定义切点 2
    @Pointcut("execution(public * com.example..*.*(..))")
    public void myPointcut() {
    }

    @Before("aopWebLog()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        startTime.set(System.currentTimeMillis());
        // 接收到请求,记录请求内容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        // 记录下请求内容
        logger.info("URL : " + request.getRequestURL().toString());
        logger.info("HTTP方法 : " + request.getMethod());
        logger.info("IP地址 : " + request.getRemoteAddr());
        logger.info("类的方法 : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
        //logger.info("参数 : " + Arrays.toString(joinPoint.getArgs()));
        logger.info("参数 : " + request.getQueryString());
    }

    @AfterReturning(pointcut = "aopWebLog()",returning = "retObject")
    public void doAfterReturning(Object retObject) throws Throwable {
        // 处理完请求,返回内容
        logger.info("应答值 : " + retObject);
        logger.info("费时: " + (System.currentTimeMillis() - startTime.get()));
    }

    //抛出异常后通知(After throwing advice) : 在方法抛出异常退出时执行的通知。
    @AfterThrowing(pointcut = "aopWebLog()", throwing = "ex")
    public void addAfterThrowingLogger(JoinPoint joinPoint, Exception ex) {
        logger.error("执行 " + " 异常", ex);
    }


    @Around("myPointcut()")
    public Object mylogger (ProceedingJoinPoint pjp) throws Throwable {
        String className = pjp.getTarget().getClass().toString();
        String methodName = pjp.getSignature().getName();
        Object[] arry = pjp.getArgs();
        ObjectMapper mapper = new ObjectMapper();
        logger.info("调用前:"+className+":"+methodName+"传递的参数为:"+mapper.writeValueAsString(arry));
        Object obj = pjp.proceed();
        logger.info("调用后"+className+":"+methodName+"返回值为:"+mapper.writeValueAsString(obj));
        return obj;
    }

}
@RestController
public class AopLogController {
//    @GetMapping("/aoptest")
//    public String aVoid(){
//        return "hello aop test";
//    }

    @GetMapping("/hello")
    public String hello (@RequestParam("name")String name,@RequestParam("age")String age){
        return "hello"+name+"age"+age;
    }
}

运行:

 可以看到日志已经打印出来了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/797043.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用网络 IP 扫描程序的原因

随着网络不断扩展以满足业务需求&#xff0c;高级 IP 扫描已成为网络管理员确保网络可用性和性能的关键任务。在大型网络中扫描 IP 地址可能具有挑战性&#xff0c;这些网络通常包括具有动态 IP、多个 DNS、DHCP 配置和复杂子网的有线和无线设备。使用可提供全面 IP 地址管理 &…

哈尔滨的全年平均气温和降水特点

哈尔滨全年平均气温和降水特点 哈尔滨是我国最靠北的省会城市&#xff0c;冬季那边特别冷&#xff0c;但夏季比较凉爽&#xff0c;下面根据中国气象台的1981-2010年的统计数据[1]&#xff0c;分析哈尔滨全年平均气温和降水的特点有以下几点&#xff1a; 1.全年最高气温在7月&…

ubuntu18.04安装autoware1.15

目录 前言一、准备工作1.安装autoware1.152.安装依赖3.把src/autoware/common/autoware_build_flags/cmake文件夹下的CUDA版本改为11.4&#xff08;或者你电脑上的版本&#xff09; 二、解决报错错误类型1错误类型2错误类型3错误类型4错误类型5错误类型6 前言 本文参考链接&am…

如何降低TCP在局域网环境下的数据传输延迟

以Ping为例。本案例是一个测试题目&#xff0c;只有现象展示&#xff0c;不含解决方案。 ROS_Kinetic_26 使用rosserial_windows实现windows与ROS master发送与接收消息_windows 接收ros1 消息 什么是ping&#xff1f; AI&#xff1a; ping是互联网控制消息协议&#xff08;…

数学建模-预测模型 神经网络

设置测试集&#xff0c;算sse&#xff0c;进行过拟合检验

无涯教程-jQuery - jQuery.get( url, data, callback, type )方法函数

jQuery.get(url&#xff0c;[data]&#xff0c;[callback]&#xff0c;[type])方法使用GET HTTP请求从服务器加载数据。 该方法返回XMLHttpRequest对象。 jQuery.get( url, [data], [callback], [type] ) - 语法 $.get( url, [data], [callback], [type] ) 这是此方法使用的…

电动汽车市场的减速,正在让小鹏汽车付出代价

来源&#xff1a;猛兽财经 作者&#xff1a;猛兽财经 总结&#xff1a; &#xff08;1&#xff09;由于价格压力上升、竞争加剧和需求减弱&#xff0c;小鹏汽车的交付量出现了明显下滑&#xff0c;6月份的交付量已经同比下降了43%。 &#xff08;2&#xff09;小鹏汽车对2023年…

ancos注册中心、网关和静态化freemarker、对象存储服务MinIO

1、docker安装ancos ①&#xff1a;docker拉取镜像 docker pull nacos/nacos-server:1.2.0②&#xff1a;创建容器 docker run --env MODEstandalone --name nacos --restartalways -d -p 8848:8848 nacos/nacos-server:1.2.0③&#xff1a;访问地址&#xff1a;http://192…

【论文阅读】The Deep Learning Compiler: A Comprehensive Survey

论文来源&#xff1a;Li M , Liu Y , Liu X ,et al.The Deep Learning Compiler: A Comprehensive Survey[J]. 2020.DOI:10.1109/TPDS.2020.3030548. 这是一篇关于深度学习编译器的综述类文章。 什么是深度学习编译器 深度学习&#xff08;Deep Learning&#xff09;编译器将…

getInputStream has already been called for this request 问题记录

问题背景 HttpServletRequest.getReader() HttpServletRequest.getInputStream() 不能在过滤器中读取一次二进制流&#xff08;字符流&#xff09;&#xff0c;又在另外一个Servlet中读取一次&#xff0c;即一个InputSteam(BufferedReader)对象在被读取完成后&#xff0c;将无…

FPGA XDMA 中断模式实现 PCIE3.0 AD7606采集 提供2套工程源码和QT上位机源码

目录 1、前言2、我已有的PCIE方案3、PCIE理论4、总体设计思路和方案AD7606数据采集和缓存XDMA简介XDMA中断模式QT上位机及其源码 5、vivado工程1--BRAM缓存6、vivado工程2--DDR4缓存7、上板调试验证8、福利&#xff1a;工程代码的获取 1、前言 PCIE&#xff08;PCI Express&am…

全方位支持图文和音视频、100+增强功能,Facebook开源数据增强库AugLy

Facebook 近日开源了数据增强库 AugLy&#xff0c;包含四个子库&#xff0c;每个子库对应不同的模态&#xff0c;每个库遵循相同的接口。支持四种模态&#xff1a;文本、图像、音频和视频。 最近&#xff0c;Facebook 开源了一个新的 Python 库——AugLy&#xff0c;该库旨在帮…

Error: unknown flag: --export 【k8s,kubernets报错】

报错情况如下&#xff1a; [rootk8smaster ~]# kubectl get deploy nginx -oyaml --export > my2.yaml Error: unknown flag: --export See kubectl get --help for usage.原因&#xff1a; --export在所使用的版本中已被移除 解决&#xff1a;去除--export即可&#xff0c…

基于Javaweb实现ATM机系统开发实战(十五)退卡和转账跳转实现

首先创建一个servlet接受和处理请求&#xff1a; package com.atm.servlet;import javax.servlet.*; import javax.servlet.http.*; import javax.servlet.annotation.*; import java.io.IOException;//用户退出 WebServlet("/logout") public class ExitServlet ex…

14、php面向对象3(final、显示调用父类构造方法、static静态变量与方法)

1、如果父类中的方法被声明为 final&#xff0c;则子类无法覆盖该方法。如果一个类被声明为 final&#xff0c;则不能被继承。 <?php class BaseClass{public function test(){echo "BaseClass::test() called".PHP_EOL;}final public function moreTesting(){e…

使用CRM进行数据分析的四大好处

使用CRM数据分析系统够帮助企业更好地了解客户需求和行为习惯&#xff0c;提供个性化的服务&#xff0c;从而提高客户满意度和忠诚度。使用CRM数据分析系统可以为企业带来一些好处&#xff0c;包括提高客户洞察力、加强营销策略、提高运营效率等。 1.提高客户洞察力&#xff1a…

【C++ 重要知识点总结】进制与编码

1 进位计数 数制 2进制----字面量0b8进制----字面量010进制—无16进制0x-字面量0x 数制转化 r进制数转化成十进制 I a n − 1 r n − 1 ⋯ a 0 r 0 I a_{n-1}\times r^{n-1} \cdots a_0 \times r^0 Ian−1​rn−1⋯a0​r0十进制整数转化r进制数——除r取余法 I r…

日志系统:一条SQL更新语句是如何执行的

知识粗粮 为什么mysql 8 把缓存给取消了&#xff1f;&#xff08;在这里我不咋说&#xff0c;很简单&#xff0c;自己去百度&#xff09; mysql 可以回复到半个月内的任意一秒的状态 sql的执行链路&#xff08;8把查询缓存彻底给搞掉了呜呜呜&#xff09; 进入正题 下面我们从…

MyBatis-Flex 是什么(一个优雅的MyBatis增强框架)

直接去看官网吧&#xff1a;MyBatis-Flex - MyBatis-Flex 官方网站 MyBatis-Flex 是一个优雅的 MyBatis 增强框架&#xff0c;它非常轻量、同时拥有极高的性能与灵活性。我们可以轻松的使用 Mybaits-Flex 链接任何数据库&#xff0c;其内置的 QueryWrapper^亮点 帮助我们极大…

集装箱装卸作业相关的知识-Part1

1.角件 Corner Fitting of Container or called Corner Casting. there are eigth of it of one container. 国家标准|GB/T 1835-2006https://openstd.samr.gov.cn/bzgk/gb/newGbInfo?hcnoD35857F2200FA115CAA217A114F5EF12 中国的国标&#xff1a;GB/T 1835-2006《系列1集…