SpringAOP(面向切面编程)和动态代理的总结

1、AOP概念:

主要功能:日志记录

2、AOP特点:

采用横向抽取机制,取代了传统纵向继承体系重复性代码。

3、AOP底层实现:

AOP底层使用动态代理实现。包括两种方式:

  • 使用JDK动态代理实现。
  • 使用cglib来实现

 jdk和cglib的区别:

只能对实现了接口的类生成代理,而不是针对类,该目标类型实现的接口都将被代理。

JDK动态代理实现:

原理是通过在运行期间创建一个接口的实现类来完成对目标对象的代理。步骤如下:

  1. 定义一个实现接口InvocationHandler的类
  2. 通过构造函数,注入被代理类
  3. 实现invoke( Object proxy, Method method, Object[] args)方法
  4. 在主函数中获得被代理类的类加载器
  5. 使用Proxy.newProxyInstance( )产生一个代理对象
  6. 通过代理对象调用各种方法

动态代理的实现:

我们先定义了接口Hello,但是我们并不去编写实现类,而是直接通过JDK提供的一个Proxy.newProxyInstance()创建了一个Hello接口对象。这种没有实现类但是在运行期动态创建了一个接口对象的方式,我们称为动态代码。JDK提供的动态创建接口对象的方式,就叫动态代理。

     每一个动态代理类的调用处理程序都必须实现InvocationHandler接口,并且每个代理类的实例都关联到了实现该接口的动态代理类调用处理程序中,当我们通过动态代理对象调用一个方法时候,这个方法的调用就会被转发到实现InvocationHandler接口类的invoke方法来调用,看如下invoke方法:
 

/**
    * proxy:代理类代理的真实代理对象com.sun.proxy.$Proxy0
    * method:我们所要调用某个对象真实的方法的Method对象
    * args:指代代理对象方法传递的参数
    */
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;

2.Proxy类就是用来创建一个代理对象的类,它提供了很多方法,但是我们最常用的是newProxyInstance方法。

 public static Object newProxyInstance(ClassLoader loader, 
                                            Class<?>[] interfaces, 
                                            InvocationHandler h)
Returns an instance of a proxy class for the specified interfaces
     that dispatches method invocations to the specified invocation
     handler.  This method is equivalent to:

这个方法的作用就是创建一个代理类对象,它接收三个参数,我们来看下几个参数的含义:

  • loader:一个classloader对象,定义了由哪个classloader对象对生成的代理类进行加载
  • interfaces:一个interface对象数组,表示我们将要给我们的代理对象提供一组什么样的接口,如果我们提供了这样一个接口对象数组,那么也就是声明了代理类实现了这些接口,代理类就可以调用接口中声明的所有方法。
  • h:一个InvocationHandler对象,表示的是当动态代理对象调用方法的时候会关联到哪一个InvocationHandler对象上,并最终由其调用。

3动态代理中核心的两个接口和类上面已经介绍完了,接下来我们就用实例来讲解下具体的用法

  • 首先我们定义一个接口People
package reflect;

public interface People {

    public String work();
}
  • 定义一个Teacher类,实现People接口,这个类是真实的对象

 

package reflect;

public class Teacher implements People{

    @Override
    public String work() {
        System.out.println("老师教书育人...");
        return "教书";
    }

}
  • 现在我们要定义一个代理类的调用处理程序,每个代理类的调用处理程序都必须实现InvocationHandler接口,代理类如下:
package reflect;

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

public class WorkHandler implements InvocationHandler{

    //代理类中的真实对象  
    private Object obj;

    public WorkHandler() {
        // TODO Auto-generated constructor stub
    }
    //构造函数,给我们的真实对象赋值
    public WorkHandler(Object obj) {
        this.obj = obj;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //在真实的对象执行之前我们可以添加自己的操作
        System.out.println("before invoke。。。");
        Object invoke = method.invoke(obj, args);
        //在真实的对象执行之后我们可以添加自己的操作
        System.out.println("after invoke。。。");
        return invoke;
    }

}
 上面的代理类的调用处理程序的invoke方法中的第一个参数proxy好像我们从来没有用过,而且关于这个参数的具体用法含义请参考我的另外一篇文章Java中InvocationHandler接口中第一个参数proxy详解
  • 接下来我们看下客户端类
package reflect;

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

public class Test {

    public static void main(String[] args) {
        //要代理的真实对象
        People people = new Teacher();
        //代理对象的调用处理程序,我们将要代理的真实对象传入代理对象的调用处理的构造函数中,最终代理对象的调用处理程序会调用真实对象的方法
        InvocationHandler handler = new WorkHandler(people);
        /**
         * 通过Proxy类的newProxyInstance方法创建代理对象,我们来看下方法中的参数
         * 第一个参数:people.getClass().getClassLoader(),使用handler对象的classloader对象来加载我们的代理对象
         * 第二个参数:people.getClass().getInterfaces(),这里为代理类提供的接口是真实对象实现的接口,这样代理对象就能像真实对象一样调用接口中的所有方法
         * 第三个参数:handler,我们将代理对象关联到上面的InvocationHandler对象上
         */
        People proxy = (People)Proxy.newProxyInstance(handler.getClass().getClassLoader(), people.getClass().getInterfaces(), handler);
        //System.out.println(proxy.toString());
        System.out.println(proxy.work());
    }
}

看下输出结果:

before invoke。。。
老师教书育人...
after invoke。。。
教书

 

 一个最简单的动态代理实现如下

 

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

interface Hello {
    void morning(String name);
}

public class Main {
    public static void main(String[] args) {
        InvocationHandler handler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println(method);
                if (method.getName().equals("morning")) {
                    System.out.println("Good morning, " + args[0]);
                }
                return null;
            }
        };
        Hello hello = (Hello) Proxy.newProxyInstance(
            Hello.class.getClassLoader(), // 传入ClassLoader
            new Class[] { Hello.class }, // 传入要实现的接口
            handler); // 传入处理调用方法的InvocationHandler
        hello.morning("Bob");
    }
}

在运行期动态创建一个interface实例的方法如下:

  1. 定义一个InvocationHandler实例,它负责实现接口的方法调用;
  2. 通过Proxy.newProxyInstance()创建interface实例,它需要3个参数:
    1. 使用的ClassLoader,通常就是接口类的ClassLoader
    2. 需要实现的接口数组,至少需要传入一个接口进去;
    3. 用来处理接口方法调用的InvocationHandler实例。
  3. 将返回的Object强制转型为接口。

动态代理实际上是JDK在运行期动态创建class字节码并加载的过程,它并没有什么黑魔法,把上面的动态代理改写为静态实现类大概长这样:

public class HelloDynamicProxy implements Hello {
    InvocationHandler handler;
    public HelloDynamicProxy(InvocationHandler handler) {
        this.handler = handler;
    }
    public void morning(String name) {
        handler.invoke(
           this,
           Hello.class.getMethod("morning"),
           new Object[] { name });
    }
}

 其实就是JDK帮我们自动编写了一个上述类(不需要源码,可以直接生成字节码),并不存在可以直接实例化接口的黑魔法。

小结

Java标准库提供了动态代理功能,允许在运行期动态创建一个接口的实例;

动态代理是通过Proxy创建代理对象,然后将接口方法“代理”给InvocationHandler完成的。

cglib动态代理实现:

针对类实现代理,对是否实现接口无要求。原理是对指定的类生成一个子类,覆盖其中的方法,因为是继承,所以被代理的类或方法最好不要声明为final类型。

  • 定义一个实现了MethodInterceptor接口的类
  • 实现其intercept()方法,在其中调用proxy.invokeSuper( )
  • 代理方式的选择:

如果目标对象实现了接口,默认情况下回采用JDK的动态代理实现AOP,也可以强制使用cglib实现AOP

如果目标对象没有实现接口,必须采用cglib库,Spring会自动在JDK动态代理和cglib之间转换

静态代理和动态代理的区别:

  • 静态代理:自己编写创建代理类,然后再进行编译,在程序运行前,代理类的.class文件就已经存在了。
  • 动态代理:在实现阶段不用关心代理谁,而在运行阶段(通过反射机制)才指定代理哪一个对象。

想要强制使用CGLIB,那么就设置<aop:config>下面的proxy-target-class属性为true

<aop:config proxy-target-class="true">
        <!-- other beans defined here... -->
</aop:config>

要是使用@AspectJ强制使用CGLIB的话,可以配置<aop:aspectj-autoproxy>下的proxy-target-class属性为true

<aop:aspectj-autoproxy proxy-target-class="true"/>

 

原理区别:

java动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。而cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。

  1. 如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP
  2. 如果目标对象实现了接口,可以强制使用CGLIB实现AOP
  3. 如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换
#开启对AOP的支持
spring.aop.auto=true
#设置代理模式 true(cglib) false(java JDK代理)
spring.aop.proxy-target-class=true

Spring Boot事务默认是使用的cglib代理,可以通过上面的方式去指定代理模式。

Spring 5 AOP 默认改用 CGLIB 了?从现象到源码的深度分析

Spring5 AOP 默认使用 Cglib 了?我第一次听到这个说法是在一个微信群里:

群聊天

真的假的?查阅文档

刚看到这个说法的时候,我是保持怀疑态度的。

大家都知道 Spring5 之前的版本 AOP 在默认情况下是使用 JDK 动态代理的,那是不是 Spring5 版本真的做了修改呢?于是我打开 Spring Framework 5.x 文档,再次确认了一下:

文档地址:https://docs.spring.io/spring/docs/5.2.0.RELEASE/spring-framework-reference/core.html#aop

 

Spring Framework 5.x 文档

简单翻译一下。Spring AOP 默认使用 JDK 动态代理,如果对象没有实现接口,则使用 CGLIB 代理。当然,也可以强制使用 CGLIB 代理。

什么?文档写错了?!

当我把官方文档发到群里之后,又收到了这位同学的回复:

文档写错了?!

SpringBoot 2.x 代码示例

为了证明文档写错了,这位同学还写了一个 DEMO。下面,就由我来重现一下这个 DEMO 程序:

运行环境:SpringBoot 2.2.0.RELEASE 版本,内置 Spring Framework 版本为 5.2.0.RELEASE 版本。同时添加 spring-boot-starter-aop 依赖,自动装配 Spring AOP。

 

public interface UserService {
    void work();
}

@Service
public class UserServiceImpl implements UserService {

    @Override
    public void work() {
        System.out.println("开始干活...coding...");
    }
}
@Component
@Aspect
public class UserServiceAspect {
    @Before("execution(* com.me.aop.UserService.work(..))")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("UserServiceAspect.....()");
    }
}

默认使用Cglib代理了?

UserServiceImpl实现了UserService接口,同时使用UserServiceAspectUserService#work方法进行前置增强拦截。

从运行结果来看,这里的确使用了 CGLIB 代理而不是 JDK 动态代理。

难道真的是文档写错了?!

@EnableAspectJAutoProxy 源码注释

在 Spring Framework 中,是使用@EnableAspectJAutoProxy注解来开启 Spring AOP 相关功能的。

Spring Framework 5.2.0.RELEASE 版本@EnableAspectJAutoProxy注解源码如下:

 

@EnableAspectJAutoProxy源码

通过源码注释我们可以了解到:在 Spring Framework 5.2.0.RELEASE 版本中,proxyTargetClass的默认取值依旧是false,默认还是使用 JDK 动态代理。

难道文档和源码注释都写错了?!

@EnableAspectJAutoProxy 的 proxyTargetClass 无效了?

接下来,我尝试使用@EnableAspectJAutoProxy来强制使用 JDK 动态代理。

运行环境:SpringBoot 2.2.0.RELEASE 版本,内置 Spring Framework 版本为 5.2.0.RELEASE 版本。

proxyTargetClass设置无效了?

通过运行发现,还是使用了 CGLIB 代理。难道@EnableAspectJAutoProxyproxyTargetClass设置无效了?

Spring Framework 5.x

整理一下思路

  1. 有人说 Spring5 开始 AOP 默认使用 CGLIB 了
  2. Spring Framework 5.x 文档和 @EnableAspectJAutoProxy源码注释都说了默认是使用 JDK 动态代理
  3. 程序运行结果说明,即使继承了接口,设置proxyTargetClassfalse,程序依旧使用 CGLIB 代理

等一下,我们是不是遗漏了什么?

示例程序是使用 SpringBoot 来运行的,那如果不用 SpringBoot,只用 Spring Framework 会怎么样呢?

运行环境:Spring Framework 5.2.0.RELEASE 版本。UserServiceImpl 和 UserServiceAspect 类和上文一样,这里不在赘述。

 

Spring Framework 5.x

 

Spring Framework 5.x使用CGLIB

运行结果表明:在 Spring Framework 5.x 版本中,如果类实现了接口,AOP 默认还是使用 JDK 动态代理。

再整理思路

  1. Spring5 AOP 默认依旧使用 JDK 动态代理,官方文档和源码注释没有错。
  2. SpringBoot 2.x 版本中,AOP 默认使用 cglib,且无法通过proxyTargetClass进行修改。
  3. 那是不是 SpringBoot 2.x 版本做了一些改动呢?

再探 SpringBoot 2.x

结果上面的分析,很有可能是 SpringBoot2.x 版本中,修改了 Spring AOP 的相关配置。那就来一波源码分析,看一下内部到底做了什么。

源码分析

源码分析,找对入口很重要。那这次的入口在哪里呢?

@SpringBootApplication是一个组合注解,该注解中使用@EnableAutoConfiguration实现了大量的自动装配。

EnableAutoConfiguration也是一个组合注解,在该注解上被标志了@Import。关于@Import注解的详细用法,可以参看笔者之前的文章:https://mp.weixin.qq.com/s/7arh4sVH1mlHE0GVVbZ84Q

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

AutoConfigurationImportSelector实现了DeferredImportSelector接口。

在 Spring Framework 4.x 版本中,这是一个空接口,它仅仅是继承了ImportSelector接口而已。而在 5.x 版本中拓展了DeferredImportSelector接口,增加了一个getImportGroup方法:

 

AutoConfigurationImportSelector#getImportGroup

在这个方法中返回了AutoConfigurationGroup类。这是AutoConfigurationImportSelector中的一个内部类,他实现了DeferredImportSelector.Group接口。

在 SpringBoot 2.x 版本中,就是通过AutoConfigurationImportSelector.AutoConfigurationGroup#process方法来导入自动配置类的。

导入配置类

通过断点调试可以看到,和 AOP 相关的自动配置是通过org.springframework.boot.autoconfigure.aop.AopAutoConfiguration来进行配置的。

 

AopAutoConfiguration源码

真相大白

看到这里,可以说是真相大白了。在 SpringBoot2.x 版本中,通过AopAutoConfiguration来自动装配 AOP。

默认情况下,是肯定没有spring.aop.proxy-target-class这个配置项的。而此时,在 SpringBoot 2.x 版本中会默认使用 Cglib 来实现。

SpringBoot 2.x 中如何修改 AOP 实现

通过源码我们也就可以知道,在 SpringBoot 2.x 中如果需要修改 AOP 的实现,需要通过spring.aop.proxy-target-class这个配置项来修改。

#在application.properties文件中通过spring.aop.proxy-target-class来配置
spring.aop.proxy-target-class=false

spring-configuration-metadata.json

这里也提一下spring-configuration-metadata.json文件的作用:在使用application.propertiesapplication.yml文件时,IDEA 就是通过读取这些文件信息来提供代码提示的,SpringBoot 框架自己是不会来读取这个配置文件的。

SringBoot 1.5.x 又是怎么样的

SringBoot 1.5.x

可以看到,在 SpringBoot 1.5.x 版本中,默认还是使用 JDK 动态代理的。

SpringBoot 2.x 为何默认使用 Cglib

SpringBoot 2.x 版本为什么要默认使用 Cglib 来实现 AOP 呢?这么做的好处又是什么呢?笔者从网上找到了一些资料,先来看一个 issue。

Spring Boot issue #5423

Use @EnableTransactionManagement(proxyTargetClass = true) #5423 https://github.com/spring-projects/spring-boot/issues/5423

在这个 issue 中,抛出了这样一个问题:

image.png

翻译一下:我们应该使用@EnableTransactionManagement(proxyTargetClass = true)来防止人们不使用接口时出现讨厌的代理问题。

这个"不使用接口时出现讨厌的代理问题"是什么呢?思考一分钟。

讨厌的代理问题

假设,我们有一个UserServiceImplUserService类,此时需要在UserContoller中使用UserService。在 Spring 中通常都习惯这样写代码:

@Autowired
UserService userService;

 

在这种情况下,无论是使用 JDK 动态代理,还是 CGLIB 都不会出现问题。

但是,如果你的代码是这样的呢:

@Autowired
UserServiceImpl userService;

 

这个时候,如果我们是使用 JDK 动态代理,那在启动时就会报错:

启动报错

因为 JDK 动态代理是基于接口的,代理生成的对象只能赋值给接口变量。

而 CGLIB 就不存在这个问题。因为 CGLIB 是通过生成子类来实现的,代理对象无论是赋值给接口还是实现类这两者都是代理对象的父类。

SpringBoot 正是出于这种考虑,于是在 2.x 版本中,将 AOP 默认实现改为了 CGLIB。

更多的细节信息,读者可以自己查阅上述 issue。

总结

  1. Spring 5.x 中 AOP 默认依旧使用 JDK 动态代理。
  2. SpringBoot 2.x 开始,为了解决使用 JDK 动态代理可能导致的类型转化异常而默认使用 CGLIB。
  3. 在 SpringBoot 2.x 中,如果需要默认使用 JDK 动态代理可以通过配置项spring.aop.proxy-target-class=false来进行修改,proxyTargetClass配置已无效。

延伸阅读

issue:Default CGLib proxy setting default cannot be overridden by using core framework annotations (@EnableTransactionManagement, @EnableAspectJAutoProxy) #12194 https://github.com/spring-projects/spring-boot/issues/12194

这个 issue 也聊到了关于proxyTargetClass设置失效的问题,讨论内容包括:@EnableAspectJAutoProxy@EnableCaching@EnableTransactionManagement。感兴趣的读者可以自行查阅 issue。

 

 

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值