Spring AOP基础&动态代理&基于JDK动态代理实现

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: Spring AOP基础&动态代理&基于JDK动态代理实现

1. 预备知识-动态代理

1.1 什么是动态代理

动态代理利用Java的反射技术(Java Reflection)生成字节码,在运行时创建一个实现某些给定接口的新类(也称"动态代理类")及其实例。

1.2 动态代理的优势

动态代理的优势是实现无侵入式的代码扩展,也就是方法的增强;让你可以在不用修改源码的情况下,增强一些方法;在方法的前后你可以做你任何想做的事情(甚至不去执行这个方法就可以)

spring中的AOP是动态代理使用的经典场景。

1.3 基于JDK动态代理实现

在基于JDK的动态代理的实现中有两个重要的类:InvocationHandler, Proxy

  • InvocationHandler
    是代理实例的调用处理程序实现的接口。每个代理实例都具有一个关联的调用处理程序。对代理实例调用方法时,将对方法调用进行编码并将其指派到它的调用处理程序的 invoke 方法。
  • Proxy
    JDK中动态生成代理类的工具类

一个动态代理的示例:

定义一个接口(基于JDK的动态代理只能使用接口)

public interface ISubject {
    void hello(String param);
}

为接口定义实现类

public class SubjectImpl implements ISubject {
    @Override
    public void hello(String param) {
        System.out.println("hello  " + param);
    }
}

实现一个代理类:

public class JDKProxy implements InvocationHandler {
    private Object target;
    public JDKProxy(Object target) {
        this.target = target;
    }
    //创建代理
    public Object newProxy() {
     return (ISubject)Proxy.newProxyInstance(
             target.getClass().getClassLoader(),
             target.getClass().getInterfaces(),
             this);
    }
    @Override   
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
       System.out.println("---------- 在业务方法调用之前可以进行前置增强   ------------");
        //利用反射机制调用方法,invoke为返回值,如果没有返回null
        Object invoke = method.invoke(target, args);
        System.out.println("---------- 在业务方法调用之前可以进行后置增强   ------------");
        return invoke;
    }
}

编写代理类实际的调用,利用Proxy类创建代理之后的Subject类

public class JDKProxyDemo {
    public static void main(String[] args) {
        ISubject subject = new SubjectImpl();
        JDKProxy subjectProxy = new JDKProxy(subject);
        ISubject proxyInstance = (ISubject)subjectProxy.newProxy();
        proxyInstance.hello("world");
    }
}

运行结果:

---------- 在业务方法调用之前可以进行前置增强   ------------
hello  world
---------- 在业务方法调用之前可以进行后置增强   ------------

2. AOP

2.1 基本概念

  • 连接点 (Joinpoint)
    程序执行过程中明确的点,如方法的调用,或者异常的抛出.
  • 目标(Target)
    被通知(被代理)的对象,如上例中的SubjectImpl
  • 通知(Advice)
    在某个特定的连接点上执行的动作,同时Advice也是程序代码的具体实现,例如一个实现日志记录的代码(通知有些书上也称为处理),可以理解为AOP真正要实现的功能
  • 代理(Proxy)
    将通知应用到目标对象后创建的对象(代理=目标+通知),请注意:只有代理对象才有AOP功能,而AOP的代码是写在通知的方法里面的,如上例中的JDKProxy
  • 切入点(Pointcut)
    多个连接点的集合,定义了通知应该应用到那些连接点。也将Pointcut理解成一个条件 ,此条件决定了容器在什么情况下将通知和目标组合成代理返回给外部程序
  • 适配器(Advisor)
    适配器=通知(Advice)+切入点(Pointcut)

AOP运行原理:目标对象只负责业务逻辑,通知只负责AOP增强逻辑(如日志,数据验证等),而代理对象则将业务逻辑而AOP增强代码组织起来(组织者)

2.2 AOP带来的好处

AOP是公用的框架代码放置的理想地方,将公共代码与业务代码分离,使我们在处理业务时可以专心的处理业务。

伪代码:

public void doSameBusiness (long lParam,String sParam){
     // 记录日志
     log.info("调用 doSameBusiness方法,参数是:"+lParam);
     // 输入合法性验证
     if (lParam<=0){
          throws new IllegalArgumentException("xx应该大于0");
     }
     if (sParam==null || sParam.trim().equals("")){
          throws new IllegalArgumentException("xx不能为空");
     }
     // 异常处理
     try{ 
    真正的业务处理
     }catch(...){
     }catch(...){
     }
     // 事务控制
     tx.commit();
 }

通过使用AOP我们可以将日志记录,数据合法性验证,异常处理等功能放入AOP中,那么在编写业务时就可以专心实现真正的业务逻辑代码。

3. Spring AOP

在spring中org.springframework.aop.framework.ProxyFactoryBean用来创建代理对象,在一般情况下它需要注入一下三个属性:

  • proxyInterfaces 代理应该实现的接口列表(List)
  • interceptorNames 需要应用到目标对象上的通知Bean的名字
  • target 目标对象 (Object)

准备工作:创建一个IBookService接口及其实现类,用于演示spring AOP开发示例:

public interface IBookService {
    // 购书
    public boolean buy(String userName, String bookName, Double price);
    // 发表书评
    public void comment(String userName, String comments);
}
public class BookServiceImpl implements IBookService {
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    public BookServiceImpl() {
        super();
    }
    public boolean buy(String userName, String bookName, Double price) {
        //logger.info("userName={},bookName={},price={}", userName, bookName, price);
        // 通过控制台的输出方式模拟购书
        logger.info(userName + " buy " + bookName + ", spend " + price);
        return true;
    }
    public void comment(String userName, String comments) {
        logger.info(userName + " say:" + comments);
    }
}

将service配置到spring配置文件中,以便于被spring管理,按自己的实际情况配置

<!-- 目标 -->
 <bean id="bookServiceTarget" class="com.zking.sp02.impl.BookServiceImpl"/>

3.1 前置通知

前置通知需要实现org.springframework.aop.MethodBeforeAdvice,前置通知将在目标对象调用前调用。示例实现购书系统AOP方式实现日志,简单打印调用的方法及参数

1)首先实现一个前置通知类,实现接口MethodBeforeAdvice,并实现接口中的before方法

public class MyMethodBeforeAdvice implements MethodBeforeAdvice {
    private Logger logger  = LoggerFactory.getLogger(this.getClass());
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        String s = "[前置通知]: " 
                + this.getClass() + "." 
                + method.getName() 
                + "将被调用,参数为:" 
                + Arrays.toString(args);
        logger.info(s);
    }
}

2)将实现的前置通知配置到Spring.xml中,一遍与被spring管理。需要根据自己的实际情况配置。

<bean id="myMethodBeforeAdvice" class="com.zking.springdemo.aop.MyMethodBeforeAdvice"/>

3)现在需要解决如何将通知和目标联系起来,需要一个组织者 - 代理

<bean id="bookService" class="org.springframework.aop.framework.ProxyFactoryBean">
        <!-- 配置代理目标 -->
        <property name="target" ref="bookServiceTarget"/>
        <!-- 配置拦截器列表,拦截器就是通知 -->
        <property name="interceptorNames">
            <list>
                <value>myMethodBeforeAdvice</value>
            </list>
        </property>
        <!-- 代理要实现的接口,代理类与被代理类需要实现相同接口 -->
        <property name="proxyInterfaces">
            <list>
                <value>com.zking.springdemo.aop.IBookService</value>
            </list>
        </property>
    </bean>

写一个测试类,测试前置通知

public class Demo {
    public static void main(String[] args) {
        ApplicationContext <u>cxt</u> = new ClassPathXmlApplicationContext("/spring.xml");
        IBookService bookService = (IBookService)cxt.getBean("bookService");
        System.out.println(bookService.getClass().getName());        
        bookService.buy("zs", "hlm", 10D);
    }
}

3.2 后置通知

在连接点正常完成后执行的通知。定义的后置通知类需要实org.springframework.aop.AfterReturningAdvice

示例:在线购书系统中,要求不修改BookServiceImpl代码的情况下增加如下功能:对买书的用户进行返利:每买本书返利10元,简单打印类似于“[后置通知] 返利10元”即可。开发步骤与前置通知类似

1) 编写一个后置通知实现类

public class MyAfterReturnAdvice implements AfterReturningAdvice {
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) 
            throws Throwable {       
        logger.info("[后置通知]:  返利10元");        
    }
}

2) 将后置通知实现类配置到spring配置文件中,以便于spring管理

<bean id="myAfterReturnAdvice" class="com.zking.springdemo.aop.MyAfterReturnAdvice"/>

3)解决如何将通知和目标联系起来,在实现前置通知时已经配置了代理对象,现在只要将后置通知也配置到拦截器列表当中即可。

<!-- 配置拦截器列表,拦截器就是通知 -->
<property name="interceptorNames">
    <list>
        <!-- 前置通知 -->
        <value>myMethodBeforeAdvice</value>
        <!-- 后置通知 -->
        <value>myAfterReturnAdvice</value>
    </list>
</property>

运行上例已经实现的测试类,查看后置通知的运行效果。

3.3 环绕通知

包围一个连接点的通知,最大特点是可以修改返回值,由于它在方法前后都加入了自己的逻辑代码,因此功能很强大。自定义的环绕通知需要实现org.aopalliance.intercept.MethodInterceptor接口。

示例:在环绕通知中输出日志和返回值

1)实现一个环绕通知,该类要实现MethodInterceptor接口

public class MyMethodInterceptor implements MethodInterceptor {
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        //获取目标对象
        Object target = invocation.getThis();
        //获取参数
        Object[] args = invocation.getArguments();
        //获取方法
        Method method = invocation.getMethod();
        logger.info("[环绕通知] 前:将调用{}.{}方法,参数为{}",
                target.getClass(),
                method.getName(), 
                Arrays.toString(args));
        //调用目标对象上的方法   
        Object val = invocation.proceed();
        logger.info("[环绕通知] 后,已调用{}.{}, 返回值:{}", 
                target.getClass(),
                method.getName(), 
                val);
        return val;
    }
}

2)在spring的配置文件中配置环绕通知,以便于spring管理

<bean id="myMethodInterceptor" class="com.zking.springdemo.aop.MyMethodInterceptor"/>

3)解决如何将通知和目标联系起来,在实现前置通知时已经配置了代理对象,现在只要将环绕通知也配置到拦截器列表当中即可。

<!-- 配置拦截器列表,拦截器就是通知 -->
<property name="interceptorNames">
    <list>
        <!--前置通知-->
        <value>myMethodBeforeAdvice</value>
        <!--后置通知-->
        <value>myAfterReturnAdvice</value>
        <!--环绕通知-->
        <value>myMethodInterceptor</value>
    </list>
</property>

运行上例已经实现的测试类,查看后置通知的运行效果。

3.4 异常通知

异常通知需要实现ThrowsAdvice接口,这个通知会在方法抛出异常退出时执行,与以上演示的前置、后置、环绕通知不同,主要有一下特点:

  • 这个接口里面没有定义方法,要求我们的类必须实现afterThrows这个方法
  • 以异常类型作为参数,无返回值

示例

1)定义一个自定义异常,继承RuntimeException运行时异常

public class PriceException extends RuntimeException {
    public PriceException() {
        super();
    }
    public PriceException(String message, Throwable cause) {
        super(message, cause);
    }
    public PriceException(String message) {
        super(message);
    }
    public PriceException(Throwable cause) {
        super(cause);
    }
}

2)创建异常通知类,该类实现ThrowsAdvice接口,并实现afterThrowing方法

public class MyThrowsAdvice implements ThrowsAdvice {
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    //以异常类型作为参数,无返回值
    public void afterThrowing(PriceException e) {
        logger.info("程序发生了PriceException异常");
    }
}

剩下的步骤是将异常通知配置到spring配置文件中,及在代理配置中加入异常通知的配置,可参考上面的环绕通知等示例。

3.5 适配器

适配器, 通过正则表达式来定义方法切入点,也就是说定义哪些方法将被拦截器处理。适配器=通知(Advice)+切入点(Pointcut)。

在配置适配器时需要使用org.springframework.aop.support.RegexpMethodPointcutAdvisor

配置适配器示例:

<!-- 配置适配器 -->
<bean id="myAdisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
    <!-- 定义正则表达式,定义需要拦截的方法名 ,本例定义了所有以buy结尾的方法  -->
    <property name="patterns">
        <list>
            <value>.*buy</value>
        </list>
    </property>
    <!-- 定义由那个通知(或者叫拦截器)来处理匹配的方法 -->
    <property name="advice">
        <ref bean="myAfterReturnAdvice"/>
    </property>
</bean>

在代理中使用刚刚配置的适配器

<!-- 将直接使用后置拦截器 ,改为使用适配器 -->
<!-- <value>myMethodAfterReturnAdvice</value> -->
<!-- 通过适配器使用后置拦截器 -->
<value>myAdisor</value>

修改代理中的拦截器列表(spring配置文件,代理部分),将配置器直接配置在拦截器列表中即可。


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
6天前
|
设计模式 Java 测试技术
spring复习04,静态代理动态代理,AOP
这篇文章讲解了Java代理模式的相关知识,包括静态代理和动态代理(JDK动态代理和CGLIB),以及AOP(面向切面编程)的概念和在Spring框架中的应用。文章还提供了详细的示例代码,演示了如何使用Spring AOP进行方法增强和代理对象的创建。
spring复习04,静态代理动态代理,AOP
|
20天前
|
Java 数据库连接 数据库
Spring基础3——AOP,事务管理
AOP简介、入门案例、工作流程、切入点表达式、环绕通知、通知获取参数或返回值或异常、事务管理
Spring基础3——AOP,事务管理
|
2月前
|
XML Java 数据格式
Spring5入门到实战------11、使用XML方式实现AOP切面编程。具体代码+讲解
这篇文章是Spring5框架的AOP切面编程教程,通过XML配置方式,详细讲解了如何创建被增强类和增强类,如何在Spring配置文件中定义切入点和切面,以及如何将增强逻辑应用到具体方法上。文章通过具体的代码示例和测试结果,展示了使用XML配置实现AOP的过程,并强调了虽然注解开发更为便捷,但掌握XML配置也是非常重要的。
Spring5入门到实战------11、使用XML方式实现AOP切面编程。具体代码+讲解
|
2月前
|
缓存 Java 开发者
Spring高手之路22——AOP切面类的封装与解析
本篇文章深入解析了Spring AOP的工作机制,包括Advisor和TargetSource的构建与作用。通过详尽的源码分析和实际案例,帮助开发者全面理解AOP的核心技术,提升在实际项目中的应用能力。
23 0
Spring高手之路22——AOP切面类的封装与解析
|
2月前
|
开发者 C# 容器
【独家揭秘】当WPF邂逅DirectX:看这两个技术如何联手打造令人惊艳的高性能图形渲染体验,从环境搭建到代码实践,一步步教你成为图形编程高手
【8月更文挑战第31天】本文通过代码示例详细介绍了如何在WPF应用中集成DirectX以实现高性能图形渲染。首先创建WPF项目并使用SharpDX作为桥梁,然后在XAML中定义承载DirectX内容的容器。接着,通过C#代码初始化DirectX环境,设置渲染逻辑,并在WPF窗口中绘制图形。此方法适用于从简单2D到复杂3D场景的各种图形处理需求,为WPF开发者提供了高性能图形渲染的技术支持和实践指导。
81 0
|
2月前
|
Java Spring XML
掌握面向切面编程的秘密武器:Spring AOP 让你的代码优雅转身,横切关注点再也不是难题!
【8月更文挑战第31天】面向切面编程(AOP)通过切面封装横切关注点,如日志记录、事务管理等,使业务逻辑更清晰。Spring AOP提供强大工具,无需在业务代码中硬编码这些功能。本文将深入探讨Spring AOP的概念、工作原理及实际应用,展示如何通过基于注解的配置创建切面,优化代码结构并提高可维护性。通过示例说明如何定义切面类、通知方法及其应用时机,实现方法调用前后的日志记录,展示AOP在分离关注点和添加新功能方面的优势。
38 0
|
2月前
|
设计模式 Java C++
揭秘!JDK动态代理VS CGLIB:一场关于Java代理界的‘宫心计’,你站哪队?
【8月更文挑战第24天】Java 动态代理是一种设计模式,允许在不改动原类的基础上通过代理类扩展功能。主要实现方式包括 JDK 动态代理和 CGLIB。前者基于接口,利用反射机制在运行时创建代理类;后者采用继承方式并通过字节码技术生成子类实现类的代理。两者在实现机制、性能及适用场景上有明显差异。JDK 动态代理适用于有接口的场景,而 CGLIB 更适合代理未实现接口的类,尽管性能更优但存在一些限制。开发者可根据需求选择合适的代理方式。
85 0
|
2月前
|
缓存 安全 Java
Spring AOP 中两种代理类型的限制
【8月更文挑战第22天】
16 0
|
2月前
|
Java Spring
|
15天前
Micronaut AOP与代理机制:实现应用功能增强,无需侵入式编程的秘诀
AOP(面向切面编程)能够帮助我们在不修改现有代码的前提下,为应用程序添加新的功能或行为。Micronaut框架中的AOP模块通过动态代理机制实现了这一目标。AOP将横切关注点(如日志记录、事务管理等)从业务逻辑中分离出来,提高模块化程度。在Micronaut中,带有特定注解的类会在启动时生成代理对象,在运行时拦截方法调用并执行额外逻辑。例如,可以通过创建切面类并在目标类上添加注解来记录方法调用信息,从而在不侵入原有代码的情况下增强应用功能,提高代码的可维护性和可扩展性。
33 1
下一篇
无影云桌面