目录
编辑一、AOP 编程
1、AOP 概念
2、AOP 编程的开发步骤
3、切面的名词解释
二、AOP 的底层实现原理
1、核心问题
2、动态代理类的创建
(1)JDK 的动态代理创建
(2)CGlib 的动态代理
(3)总结
3、Spring 工厂如何加工原始对象
三、基于注解的 AOP 编程
1、基于注解的 AOP 编程的开发步骤
2、细节
(1)切入点复用
(2)动态代理的创建方式
四、AOP 开发中的坑
五、AOP 阶段知识总结
AOP (Aspect Oriented Programing)面向切面编程
以切面为基本单位的程序开发,通过切面间的彼此协同,相互调用,完成程序的构建
面向切面编程 = Spring 动态代理开发
切面 = 切入点 + 额外功能
OOP (Object Oriented Programing)面向对象编程
以对象为基本单位的程序开发,通过对象间的彼此协同,相互调用,完成程序的构建
POP (Producer Oriented Programing)面向过程编程
以过程为基本单位的程序开发,通过过程间的彼此协同,相互调用,完成程序的构建
AOP 的概念:本质就是 Spring 动态代理开发,通过代理类为原始类(目标类)增加额外功能,利于原始类的维护
注意:AOP 编程不可能代理 OOP 编程
AOP 本质上就是 Spring 的动态代理开发,那么它的开发步骤和 Spring 的动态代理开发是完全一样的:
1、原始对象
2、额外功能(MethodInterceptor)
3、切入点
4、组装切面(额外功能 + 切入点)
切面 = 切入点 + 额外功能
为什么把这两个的组合,叫做切面呢?
在几何学上:面 = 点 + 相同的性质
切面也是由 点 来构成的,它们所具有的相同性质,就是 额外功能
1、AOP 如何创建动态代理类?
前面我们学过,所谓的动态代理类,是依附于动态字节码技术,那么动态字节码技术到底是怎么通过编码,让我们把动态代理创建出来的呢?
2、Spring 工厂如何加工如何加工创建代理对象
Spring 是如何实现 通过原始对象的 id 值,最终获得的是代理对象 的呢?
对于 Spring 来讲,在动态代理类的创建过程中有两种方式:
1、JDK 的动态代理创建
2、CGlib 的动态代理
Proxy.newProxyInstance 方法参数详解
类加载器的作用:
1、通过类加载器把对应类的字节码文件加载到 JVM 中
2、通过类加载器,创建类的 Class 对象,进而创建这个类的对象
比如:如果我们想创建一个 User 类的 user 对象
那么我们首先先要通过类加载器创建一个 User 类的 Class 对象,进而才可以通过 new User() 的方法创建 user 对象
类加载器的运行过程:
假设此时我们想创建 User 类的对象,那么第一步就得开发这个 User 类,也就是创建它的 .java 文件,之后我们会对它进行相应的编译,最后编译成 .class 文件,而 .class 文件里存放的就是它所对应的字节码文件
我们要想创建 User 类对应的对象,就得把 User 类对应的字节码加载到 JVM 虚拟机当中,这个加载实际上就是类加载器完成的(类加载器的第一个作用)
然后,我们先得创建 User 类的 Class 对象(类加载器的第二个作用), 然后我们就可以根据之前的知识,去通过 new 对象来创建 User 对象
如何获得 类加载器?
虚拟机会为每一个类的 .class 文件,自动分配与之对应的类加载器
动态代理:
动态代理,实际上也是在虚拟机当中去获得动态代理类,进而创建代理对象
但是动态代理类是没有源文件,没有字节码文件的,那么动态代理类是怎么获取这个类所对应的字节码来创建对象的呢?
动态代理,是通过动态字节码技术,来创建字节码的
我们之前学到的 Proxy.newProxyInstance (classloader , interfaces , invocationhandler) 就是动态字节码技术
生成了对应动态代理的字节码之后,就直接把字节码写在了 JVM 虚拟机里面
要想创建代理类的对象,就必须得先获得代理类的 class 对象,这个过程就需要类加载器的介入
但是此时没有 .class 文件,JVM 虚拟机就不会分配类加载器,但是我们又需要类加载器
所以我们可以借用一个 类加载器
所以这也是为什么我们在创建动态代理的时候,要指定 类加载器,这个类加载器是借用的,目的是完成动态代理类 Class 对象的创建
public class TestJDKProxy {
public static void main(String[] args) {
//1、创建原始对象
UserService userService = new UserServiceImpl();
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("-------log---------");
//原始对象的方法运行
Object ret = method.invoke(userService,args);
// 方法本身 方法属于哪个对象 方法的参数
return ret;
}
};
//2、JDK 创建动态代理
UserService userService1Proxy = (UserService) Proxy.newProxyInstance(TestJDKProxy.class.getClassLoader(),userService.getClass().getInterfaces(),handler);
userService1Proxy.login("xiaoahei","123456");
userService1Proxy.register(new User());
}
}
首先,得提供我们所说的原始对象,而原始对象在 JDK 动态代理的过程中,我们必须得让它实现一个接口
当接口有了之后,我们得去实现这个接口
JDK 的动态代理中,原始对象和代理对象必须实现相同的接口
原因:
1、保证 代理类 和 原始类 方法一致,
2、代理类中可以提供新的实现:额外功能 + 对应原始方法
再来看看 CGlib 的动态代理:
假设此时有一个原始类,它没有实现任何的接口,我们想为这个没有实现任何接口的原始类,去创建它所对应的代理类,此时我们该怎么做呢?
CGlib 要求所创建的代理类,要去继承原始类
此时,也能让代理类和原始类有相同的原始方法,从而在 login 和 register 中加入额外功能
CGlib 创建动态代理的原理:通过父子继承关系创建代理对象,原始类作为父类,代理类作为子类,这样既可以保证二者方法一致,同时还可以在代理类中,提供新的实现(额外功能 + 原始方法)
C3Glib 的编码:
public class TestCglib extends UserService{
public static void main(String[] args) {
//1、创建原始对象
UserService userService = new UserService();
//2、通过 CGlib 的方法,创建动态代理对象
Enhancer enhancer = new Enhancer();
enhancer.setClassLoader(TestCglib.class.getClassLoader());//类加载器
enhancer.setSuperclass(userService.getClass());//父类
MethodInterceptor interceptor = new MethodInterceptor() {
//等同于 invocationhandler 中的 invoke 方法
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("-----cglib log--------");
Object ret = method.invoke(userService,args);
return ret;
}
};
enhancer.setCallback(interceptor);//额外功能
UserService userService1Proxy = (UserService) enhancer.create();
userService1Proxy.login("xiaohei","123456");
userService1Proxy.register(new User());
}
}
JDK 代理 依附 Proxy.newProxyInstance( ) ,通过接口创建代理的实现类
CGlib 代理 依附 Enhance ,通过继承父类创建的代理类
我们先来回顾一下 BeanPostProcessor
在 Spring 创建一个对象的时候,比如:User ,那么创建完对象的时候,Spring 工厂可以通过 BeanPostProcessor 来对我们所创建的对象进行加工,加工完成之后,把最终加工好了的对象返回给调用者,调用者就享受到了 Spring 为我们加工的 User 对象
实际上,动态代理的创建,实际上也是通过 BeanPostProcessor 进行加工的
我们再来看看,动态代理结合了 BeanPostProcessor 之后,我们这个程序变成了什么样:
创建代理过程当中,Spring 通过 BeanPostProcessor 完成了对 UserService 这个原始对象的加工
Spring 通过 userService 获得到了代理对象,实际上这个过程中,就涉及到了对 UserServiceImpl 的一个加工过程,这个加工还是通过 BeanPostProcessor 来完成的
创建了 UserService 的原始对象,调用了 UserService 的构造方法,然后 Spring 把 userService 对象创建出来了,接下类对其进行初始化操作,再交给 after 方法进行加工,其中调用了 Proxy.newProxyInstance( ) 方法,加工成我们最终所需要的代理对象了
编码:
public class ProxyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("-------- new log --------");
Object ret = method.invoke(bean,args);
return ret;
}
};
return Proxy.newProxyInstance(ProxyBeanPostProcessor.class.getClassLoader(),bean.getClass().getInterfaces(),handler);
}
}
既然是 AOP 编程,那么就应该遵从AOP 的开发步骤;
1、原始对象
2、额外功能
3、切入点
4、组装切面
我们可以把切面想象成一个类,所以在基于注解的 AOP 编程中,所对应的切面就是一个切面类
要想表达一个类是切面类,就要为其加上 @Aspect 注解
@Aspect
public class MyAspect {
/*
加上 @Around 就相当于 MethodInterceptor
此时,around 方法就相当于 invoke 方法
ProceedingJoinPoint joinPoint 就等同于 MethodInvocation invocation,代表的是原始方法
*/
@Around("execution(* login(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("----aspect log----");
Object ret = joinPoint.proceed();
return ret;
}
}
定义好了这个切面是切面类,最后用的一定是它的对象,所以后续我们还得在 Spring 的配置文件当中,通过 Spring 的工厂来创建这个类的对象
此时这个切面里面,既体现了额外功能,又体现了切入点
最后一个环节:此时我们要告诉 Spring ,我们现在要基于注解的形式,来进行 AOP 编程了,所以此时我们要增加一个新的标签:
@Around("execution(* login(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("----aspect log----");
Object ret = joinPoint.proceed();
return ret;
}
@Around("execution(* login(..))")
public Object around1(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("----aspect tx----");
Object ret = joinPoint.proceed();
return ret;
}
虽然它可以达到我们想要的效果
在这个切面当中,实际上我们为我们的 login 方法加额外功能 的过程中,这个切入点表达式实际上会存在冗余的,冗余会有两个问题:
1、同样的东西,我们写了两次,会影响到开发效率
2、后续维护的过程中,两边都得变,所以不方便修改
切入点复用,就可以解决上述问题
所谓的切入点复用,就是把我们的切入点的配置,提取到一个独立的函数上
切入点复用:在切面类中,定义一个函数,该函数上面加上 @Pointcut 注解,定义切入点表达式,后面更加有利于切入点的复用
@Pointcut("execution(* login(..))")
public void myPointCut(){};
@Around(value = "myPointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("----aspect log----");
Object ret = joinPoint.proceed();
return ret;
}
@Around(value = "myPointCut()")
public Object around1(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("----aspect tx----");
Object ret = joinPoint.proceed();
return ret;
}
AOP 底层实现中有两种代理创建方式:
JDK 通过实现接口来做新的实现类的方式,来创建代理对象
CGlib 通过继承父类的方式,来做新的子类,来创建最终的代理对象的
默认情况下,AOP 编程底层应用的是 JDK 的动态代理创建方式
如果切换成 CGlib ,应该怎么办呢?
基于注解 AOP 开发
proxy-target-class 默认情况下是 false(JDK 的动态代理),当我们改成 true 的时候,就可以更改成 CGlib 的方式了
基于传统的 AOP 开发:
在 同一个业务类中 ,进行业务方法间的相互调用,只有最外层的方法,才是加入了额外功能的方法(内部的方法,通过普通的方式调用,都调用的是原始方法),如果想让内层的方法也调用代理对象的方法,就要通过 ApplicationContextAware 来获得工厂,进而获得代理对象
@Override
public void login(String name, String password) {
System.out.println("UserServiceImpl.login");
}
@Override
public void register(User user) {
System.out.println("UserServiceImpl.register");
//调用的是原始对象的 login 方法,就只能完成核心功能
//但是我们真正的设计目的是:想调用代理对象的 login 方法 ---> 额外功能
this.login("xiaohei","123456");
}
由于 Spring 工厂是重量级资源,所以一个应用中,我们只能创建一个工厂
因此此处我们不该再创建一个工厂,而是从 测试类 中获取到已经创建好了的工厂,直接使用即可
那么怎么拿到已经创建好了的工厂呢?
让当前类再实现 ApplicationContextAware 接口,并通过接口中的方法获取到 Spring 工厂
public class UserServiceImpl implements UserService, ApplicationContextAware {
private ApplicationContext ctx ;
@Override
public void login(String name, String password) {
System.out.println("UserServiceImpl.login");
}
@Override
public void register(User user) {
System.out.println("UserServiceImpl.register");
//调用的是原始对象的 login 方法,就只能完成核心功能
//但是我们真正的设计目的是:想调用代理对象的 login 方法 ---> 额外功能
proxy.UserService userService = (proxy.UserService) ctx.getBean("userService");
userService.login("xiaohei","123456");
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.ctx = applicationContext;
}
}