什么是 AOP
- AOP 为 Aspect Oriented Programming 的缩写,意思为面向切面编程,是通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。
- AOP 是 OOP (面向对象)的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
- AOP 可以通过编译方式和运行期动态代理实现在不修改源代码的情况下,为程序统一添加/增强功能的一种技术
- 如果说面向对象编程是关注将需求功能划分为不同的并且相对独立,封装良好的类,并且让它们有着属于自己的行为,依靠继承和多态等来定义彼此的关系的话,那么面向切面编程则是希望能够将通用需求功能从不相关的类当中抽离出来,能够使得很多类共享一个行为,一旦发生变化,不必修改很多类,而只需要修改这个行为即可
- 面向对象编程思想好像一个父类的思想,面向切面编程思想更像一个接口思想
AOP 的作用及其优势
-
作用:在程序运行期间,在不修改源码的情况下对方法进行功能增强
-
优势:减少重复代码,提高开发效率,并且便于维护
-
解耦合:将日志记录,性能统计,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候,不影响业务逻辑的代码
-
AOP 是一个概念,并没有设定具体语言的实现,他能克服那些只有单继承特性语言的缺点(如java)。可以简单的把AOP理解成一种横切增强操作,就像临时查酒驾一样,在程序里面的横切行为大致如下:
- 日志记录,跟踪,优化和监控
- 性能的统计,优化
- 事务的处理
- 持久化
- 异常捕捉及处理
- 资源池(如数据库连接池)的管理
- 系统统一的认证,权限管理等
- 针对具体行业应用的横切行为
AOP 的底层实现
实际上,AOP 的底层是通过 Spring 提供的的动态代理技术实现的。在运行期间,Spring通过动态代理技术动态的生成代理对象,代理对象方法执行时进行增强功能的介入,在去调用目标对象的方法,从而完成功能的增强。
也就是业务代码该怎么写就怎么写,在需要增强业务代码的时候,可以通过AOP动态的将工具类代码植入进去
AOP 的动态代理技术
常用的动态代理技术
JDK 代理 : 基于接口的动态代理技术
cglib 代理:基于父类的动态代理技术
JDK 的动态代理
实现逻辑:
- 目标类必须是实现一个接口
- 代理类是在内存中创建的一个同样实现上述接口的类(目标类和代理类是兄弟关系)
- 因为实现相同接口,所以两者方法个数及每个方法对应的声明一致,在生成的代理类中做目标类方法的增强,代理类的方法会调用目标方法执行原来具体的功能,同时会有自己的增强代码,目标方法+代理的增强代码共同构成了增强后的代理方法
①目标类接口
public interface TargetInterface {
public void method();
}
②目标类
public class Target implements TargetInterface {
public void method() {
System.out.println("Target running....");
}
}
③动态代理代码
public class JDKDynamicProxy {
public static void main(String[] args) {
// 1. 创建目标对象:目标对象类 目标对象 = new 目标对象类();
final Target target = new Target();
// 2. 创建代理对象
// 该方法返回值即为动态代理对象,该代理对象将方法调用分派调用处理程序
TargetInterface proxyTarget = (TargetInterface) Proxy.newProxyInstance(
// 类加载器,第三类加载器,可以使用任意自定义的类获取
// 目标对象.getClass().getClassLoader()
target.getClass().getClassLoader(),
// 目标对象实现的所有接口,让生成的代理对象,也实现相同接口,拥有和目标对象相同的方法
// 目标对象.getClass().getInterfaces()
target.getClass().getInterfaces(),
// 代理对象调用方法的时候,都是通过这个来处理,最终由处理器的invoke方法执行
new InvocationHandler() {
// proxy: 动态代理对象,我们一般不用,让程序使用
// method: 目标方法,目标对象的某个被增强的方法的包装类对象
// args: 目标方法的参数列表
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
// 前置增强(在原方法运行前,添加代码,以增强/修改方法体)
System.out.println("前置增强代码...");
// 目标方法的返回值 = Method.invoke(目标对象, 目标方法的参数列表);
Object invoke = method.invoke(target, args);
// 后置增强(在原方法运行后,添加代码,以增强/修改方法体)
System.out.println("后置增强代码...");
// 这里可以增强(修改)原方法的返回值
// return 被增强方法的返回值 或 null;
return invoke;
}
});
//3. 调用代理对象方法
// 调用的是被代理对象的方法。我们并没有编写一个类(代理类),让其实现TargetInterface接口。
// 但是jdk动态代理会在程序运行时动态的生成一个代理类及其对象,并调用增强方法。
// 增强方法会执行增强代码,同时也会调用被增强方法以实现原有功能
// 动态代理对象.增强的方法(方法名与增强前相同)();
proxyTarget.method();
}
}
④ 调用代理对象的方法测试
//3. 调用代理对象方法
proxyTarget.method();
cglib 的动态代理
实现逻辑:
- “代理”的目的是构造一个和被代理的对象(目标对象)有同样行为(行为相同,但行为能力已被增强)的对象,一个对象的行为是在类中定义的,对象只是类的实例,所有构造代理,不一定非得通过持有,包装对象这一种方式
- 与JDK动态代理中的代理类和目标类需要实现同一个接口不同,cglib动态代理中代理类不需要和目标类实现同一个接口,而是代理类继承了目标类,通过“继承”可以继承父类所有的公开方法,然后可以重写这些方法,在重写时,对这些方法增强,这就是cglib的思想,当然这些增强都是程序运行时在内存中进行的
①目标类
public class Target {
public void method() {
System.out.println("Target running....");
}
}
②动态代理代码
Target target = new Target(); //创建目标对象
// 创建增强器 增强器类 增强器引用 = new 增强器类();
Enhancer enhancer = new Enhancer(); //创建增强器
// 使用增强器设置父类(父类就是目标类)enhancer.setSuperclass(Class clazz)
// 增强器引用.设置父类(父类字节码文件对象);
enhancer.setSuperclass(Target.class); //设置父类
// 使用增强器设置回调(该方法无返回值)enhancer.setCallback(Callback callback)
/*
* 在回调是需要传入一个Callback接口对象,通过创建实现了MethodInterceptor接口的匿名内部类的对
* 象来实现,创建对象时,需要明确方法的增强逻辑,增强逻辑的代码需要写在实现了MethodInterceptor
* 接口的类的intercept()方法中,可以实现前置,后置,参数列表和返回值的增强
*/
enhancer.setCallback(new MethodInterceptor() { //设置回调
/*
o 代理类对象,即this
method 父类方法包装对象
objects 被增强方法的参数
methodProxy 子类增强方法包装对象
*/
@Override
public Object intercept(Object o, Method method, Object[] objects,
MethodProxy methodProxy) throws Throwable {
System.out.println("前置代码增强....");
// 这里建议使用methodProxy.invokeSuper(proxy, args)
// 不要使用method对象,避免出现递归调用造成内存溢出
Object invoke = method.invoke(target, objects);
System.out.println("后置代码增强....");
return invoke;
}
});
// 使用增强器创建对象,返回的就是代理类对象(子类对象)enhancer.creat()
Target proxy = (Target) enhancer.create(); //创建代理对象
③调用代理对象的方法测试
//测试,当调用接口的任何方法时,代理对象的代码都无序修改
proxy.method();
AOP 相关概念
- Spring的 AOP 实现底层就是对上面的动态代理的代码进行封装,封装后我们只需要对需要关注的部分进行代码编写,并通过配置的方式完成指定目标的方法增强
- AOP 相关的术语,常用的术语如下:
- Target(目标对象):代理的目标对象,目标类,被代理(被增强)的类
- Proxy (代理):一个类被 AOP 织入增强后,就产生一个结果代理类,代理(增强)之后的类
- Joinpoint(连接点):所谓连接点是指那些被拦截到的点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点,可以被拦截的方法(有很多)
- Pointcut(切入点):所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义,(目标方法,拦截到并被增强的方法叫切点)
- Advice(通知/ 增强):所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知,(增强方法,拦截到方法之后,所要做的具体的增强内容)
- Aspect(切面):是切入点和通知(引介)的结合(目标方法 + 增强方法)
- Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程。spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入,(是一个过程,目标方法 + 增强方法结合在一起的动作叫做织入)
- 补充:将增强应用到切点的过程叫做织入,Spring采用动态代理织入,Aspectj使用编辑器织入和类装载器织入
- Aspectj是一个基于java语言的AOP框架,Spring2.0开始,Spring AOP引入对Aspectj扩展了java语言,提供了一个专门的编译器,在编译时提供横向代码的织入
- 早期Spring AOP开发使用的是自己的AOP技术,目前阶段 Spring AOP开发通常使用Aspectj技术来实现
AOP 开发明确的事项
- 需要编写的内容
- 编写核心业务代码(目标类的目标方法)
- 编写切面类,切面类中有通知(增强功能方法,增强方法需要写在切面类内部)
- 在配置文件中,配置织入关系,即将哪些通知与哪些连接点进行结合
- AOP 技术实现的内容
- Spring 框架监控切入点方法的执行,一旦监控到切入点方法被运行,使用代理机制,动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行
- Spring 框架监控切点方法的执行,通过配置指定哪些方法是切点,一旦监控到切点方法被执行,就使用动态代理创建(切点所属类的)目标对象的代理对象,然后根据通知(增强)类别(前置增强,后置增强),在代理对象的对应位置,将通知(增强)的内容织入,完成完整的代码逻辑运行
- AOP 底层使用哪种代理方式
- 在Spring中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式
- 有父接口:选择JDK动态代理
- 无父接口:选择cglib动态代理
- 在Spring中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式
基于 XML 的 AOP 开发
①导入 AOP 相关坐标
<!--导入spring的context坐标,context依赖aop-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.5.RELEASE</version>
</dependency>
<!-- aspectj的织入 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
②创建目标接口和目标类(内部有切点)
public interface TargetInterface {
public void method();
}
public class Target implements TargetInterface {
@Override
public void method() {
System.out.println("Target running....");
}
}
③创建切面类(内部有增强方法)
public class MyAspect {
//前置增强方法
public void before(){
System.out.println("前置代码增强.....");
}
}
④将目标类和切面类的对象创建权交给 spring
<!--配置目标类-->
<bean id="target" class="cs.wy.aop.Target"></bean>
<!--配置切面类-->
<bean id="myAspect" class="cs.wy.aop.MyAspect"></bean>
⑤在 applicationContext.xml 中配置织入关系,配置切点表达式和前置增强的织入关系
<aop:config>
<!--引用myAspect的Bean为切面对象-->
<aop:aspect ref="myAspect">
<!--配置Target的method方法执行时要进行myAspect的before方法前置增强-->
<aop:before method="before" pointcut="execution(public void cs.wy.aop.Target.method())"></aop:before>
</aop:aspect>
</aop:config>
XML 配置 AOP 详解
1) 切点表达式的写法
表达式语法:
execution([修饰符] 返回值类型 包名.类名.方法名(参数))
-
访问修饰符可以省略
-
返回值类型、包名、类名、方法名可以使用星号* 代表任意
-
包名与类名之间一个点 . 代表当前包下的类,两个点 … 表示当前包及其子包下的类
-
参数列表可以使用两个点 … 表示任意个数,任意类型的参数列表
例如:
<!-- aop包下Target类下无参数的method方法-->
execution(public void cs.wy.aop.Target.method())
execution(void cs.wy.aop.Target.*(..))
execution(* cs.wy.aop.*.*(..))
execution(* cs.wy.aop..*.*(..))
execution(* *..*.*(..))
2) 通知的类型
通知的配置语法:
<aop:通知类型 method=“切面类中方法名” pointcut=“切点表达式"></aop:通知类型>
3) 切点表达式的抽取
当多个增强的切点表达式相同时,可以将切点表达式进行抽取,在增强中使用 pointcut-ref 属性代替 pointcut 属性来引用抽取后的切点表达式。
<aop:config>
<!--引用myAspect的Bean为切面对象-->
<aop:aspect ref="myAspect">
<aop:pointcut id="myPointcut" expression="execution(* cs.wy.aop.*.*(..))"/>
<aop:before method="before" pointcut-ref="myPointcut"></aop:before>
</aop:aspect>
</aop:config>
基于注解的 AOP 开发
①创建目标接口和目标类(内部有切点)
public interface TargetInterface {
public void method();
}
public class Target implements TargetInterface {
@Override
public void method() {
System.out.println("Target running....");
}
}
②创建切面类(内部有增强方法)
public class MyAspect {
//前置增强方法
public void before(){
System.out.println("前置代码增强.....");
}
}
③将目标类和切面类的对象创建权交给 spring
@Component("target")
public class Target implements TargetInterface {
@Override
public void method() {
System.out.println("Target running....");
}
}
@Component("myAspect")
public class MyAspect {
public void before(){
System.out.println("前置代码增强.....");
}
}
④在切面类中使用注解配置织入关系
@Component("myAspect")
@Aspect
public class MyAspect {
@Before("execution(* com.itheima.aop.*.*(..))")
public void before(){
System.out.println("前置代码增强.....");
}
}
⑤在配置文件中开启组件扫描和 AOP 的自动代理
<!--组件扫描-->
<context:component-scan base-package="cs.wy.aop"/>
<!--aop的自动代理-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
注解配置 AOP 详解
1) 注解通知的类型
通知的配置语法:@通知注解(“切点表达式")
2) 切点表达式的抽取
同 xml配置
aop 一样,我们可以将切点表达式抽取。抽取方式是在切面内定义方法,在该方法上使用@Pointcut注解定义切点表达式,然后在在增强注解中进行引用。具体如下:
@@Component("myAspect")
@Aspect
public class MyAspect {
@Before("MyAspect.myPoint()")
public void before(){
System.out.println("前置代码增强.....");
}
@Pointcut("execution(* com.itheima.aop.*.*(..))")
public void myPoint(){}
}