【Spring学习笔记】AOP及Spring配置详解
什么是AOP,如何开发AOP开发
AOP就是Aspect Oriented Programming,面向切面编程,是通过预编译方式和运行期间动态代理(不修改源码情况下对目标代码进行加强,实现解耦合)实现程序功能的统一维护的一种技术。
AOP的作用及其优势
AOP是OOP(面向对象编程)的延续,是Spring框架中的重要内容,是函数式编程的一种衍生范型,可以利用配置的方式让多个类同时具有某个功能。使用AOP可以降低业务逻辑各部分的耦合度,提高代码可重用性,同时提高开发效率。
- 总结:
- 作用:在程序运行期间,在不修改源码的情况下对方法进行加强
- 优势:减少重复代码,提高开发效率,降低维护成本
AOP的底层实现
AOP底层是通过Spring提供的动态代理技术去实现的。在运行期间,Spring通过动态代理技术动态地生成代理对象,代理对象方法执行时进行增强功能的接入,再调用目标对象的方法,从而实现功能的增强。
以下是其底层实现的代码,如果不是进阶深入的研究,以下代码了解即可。
AOP的动态代理技术
在Spring中,如果目标类实现了接口,那么使用JDK动态代理,否则使用cglib动态代理,其底层的运行机制如下图。
JDK代理:基于接口的动态代理
public class ProxyTest {
public static void main(String[] args) {
//目标对象
final Target target = new Target();
//增强对象
Advice advice = new Advice();
//返回值就是动态代理生成的代理对象
TargetInterface proxy = (TargetInterface) Proxy.newProxyInstance(
target.getClass().getClassLoader(), //目标对象类加载器
target.getClass().getInterfaces(),//目标对象相同的字节码对象数组
new InvocationHandler() {
//调用代理对象的任何方法,实质上都是执行invoke方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
advice.before();//前置增强
Object invoke = method.invoke(target, args);//执行目标方法
advice.after();//后置增强
return invoke;
}
}
);
//调用代理对象的方法
proxy.save();
}
}
cglib代理:基于父类的动态代理技术
public class ProxyTest {
public static void main(String[] args) {
//目标对象
final Target target = new Target();
//增强对象
Advice advice = new Advice();
//返回值就是动态代理生成的代理对象,基于cglib
//1. 创建增强器
Enhancer enhancer = new Enhancer();
//2. 设置父类(目标)
enhancer.setSuperclass(Target.class);
//3. 设置回调
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
advice.before();//执行前置
Object invoke = method.invoke(target, args);//执行目标
advice.after();//执行后置
return invoke;
}
});
//4. 创建代理对象
Target proxy = (Target) enhancer.create();
proxy.save();
}
}
AOP相关概念
以上的代码可能看不太懂,但没关系。我们只需要知道,Spring的AOP实现底层就是对以上动态代理的代码进行了封装,封装后我们只需要对关注的部分进行编写,并通过配置的方式完成指定目标的方法增强即可,并不需要我们自己编写以上的代码。
但AOP还有相关的一些术语需要我们去掌握理解,这里将其列出来,我们先大致认识一下,在以后的学习过程中我们会越来越深入地理解这些概念:
- Target(目标对象):代理的目标对象
- Proxy(代理):一个类被AOP织入增强之后产生的结果代理类
- Joinpoint(连接点):指那些被拦截到的点(可以进行增强的方法),Spring中只支持方法类型的连接点。
- Pointcut(切入点 / 切点):指我们要对哪些Joinpoint进行拦截的定义(真正被增强的方法)。
- Advice(通知 / 增强):对拦截到的方法额外做的事(也是一个方法)。
- Aspect(切面):切入点和增强的结合,即被增强后的方法。
- Weaving(织入):将切入点和增强结合的过程。指把增强应用到目标对象来创建新的代理对象的过程。Spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。
AOP开发明确的事项
需要编写的内容
- 编写核心业务代码(即编写切点,目标类的目标方法,要被增强的方法)
- 编写切面类,切面类中包括增强方法(即编写增强)
- 在配置文件中配置织入关系,即将那些增强和那些连接点进行结合
AOP技术实现的内容
Spring框架监控切入点方法的执行,一旦监控到切入点方法被运行,就使用代理机制,动态创建目标对象的代理对象,根据增强类型,在代理对象的对应位置,将增强对应的功能织入,完成完整的代码逻辑运行。
基于xml 的AOP开发
开发步骤
- 导入AOP相关坐标
- 创建目标接口和目标类(内部有切点)
- 创建切面类(内部有增强方法)
- 将目标类和切面类的对象创建权交给Spring
- 在applicationContext.xml中配置织入关系
- 测试代码
导入AOP相关坐标
Spring本身虽然有其对AOP的实现,但相对而言,第三方的AOP配置AspectJ更为优秀,Spring官方也更推荐开发者使用AspectJ进行开发,因此此处在pom.xml中导入的是AspectJ。
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.4</version>
</dependency>
创建目标接口和目标类
目标接口TargetInterface.java
public interface TargetInterface {
public void save();
}
目标类Target.java
public class Target implements TargetInterface {
@Override
public void save() {
System.out.println("save...");
}
}
创建切面类
-
增强语法及类型
<!-- 语法,切点表达式在文章后面有说明到 --> <aop:增强类型 method="切面类中的方法名" pointcut="切点表达式" />
名称 标签 说明 前置增强 <aop:before> 指定的方法在切入点方法之前执行 后置增强 <aop:after-returning> 指定的方法在切入点方法之后执行 环绕增强 <aop:around> 指定的方法在切入点方法之前和之后都执行 异常抛出增强 <aop:throwing> 指定的方法在切入点方法出现异常时执行 最终增强 <aop:after> 指定的方法无论切入点方法是否出现异常都执行 MyAspect.java
public class MyAspect {
public void before() {
System.out.println("前置增强...");
}
public void afterReturning() {
System.out.println("后置增强...");
}
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("环绕前增强...");
Object proceed = pjp.proceed();
System.out.println("环绕后增强...");
return proceed;
}
public void afterThrowing() {
System.out.println("异常抛出增强...");
}
public void after() {
System.out.println("最终增强...");
}
}
将目标类和切面类的对象创建权交给Spring
将目标类和切面类的Bean对象放入Spring容器中,此时Spring将它们视为同等的Bean对象,而并不知道它们哪一个是目标对象,哪一个是切面对象,需要等后面织入关系配置完成之后Spring才能将它们区分。
applicationContext.xml
<!-- 目标对象 -->
<bean id="target" class="com.water.aop.Target"></bean>
<!-- 切面对象 -->
<bean id="myAspect" class="com.water.aop.MyAspect"></bean>
<!-- 配置织入:哪些方法需要进行哪些增强(前置、后置...) -->
配置织入关系
配置织入,就是告诉Spring哪些方法(切点)有哪些增强(前置、后置...)。
在配置织入关系之前,我们需要导入aop的命名空间,这在我之前一篇IOC学习笔记中有相关介绍,这里不展开细讲,下面来看一下怎么配置织入关系。
applicationContext.xml
<aop:config>
<!-- 声明切面 -->
<aop:aspect ref="myAspect">
<!-- 切面:切点 + 增强 -->
<!-- 这里使用around增强代替before和after-returning增强
<aop:before method="before" pointcut="execution(public void com.water.aop.*.*(..))" />
<aop:after-returning method="afterReturning" pointcut="execution(public void com.water.aop.*.*(..))" />
-->
<aop:around method="around" pointcut="execution(public void com.water.aop.*.*(..))"/>
<aop:after-throwing method="afterThrowing" pointcut="execution(public void com.water.aop.*.*(..))"/>
<aop:after method="after" pointcut="execution(public void com.water.aop.*.*(..))"/>
</aop:aspect>
</aop:config>
可以看到,其中的切点表达式重复出现了很多次,于是就有了相当于的解决方案:将切点表达式抽取出来,下一次要用的时候直接进行引用。这样做的好处是方便维护和简化书写。所以,以上的xml配置还可以写成下面这样。
<aop:config>
<!-- 声明切面 -->
<aop:aspect ref="myAspect">
<!-- 抽取切点表达式 -->
<aop:pointcut id="myPointcut" expression="execution(public void com.water.aop.*.*(..))"/>
<aop:around method="around" pointcut-ref="myPointcut"/>
<aop:after-throwing method="afterThrowing" pointcut-ref="myPointcut"/>
<aop:after method="after" pointcut-ref="myPointcut"/>
</aop:aspect>
</aop:config>
pointcut中的特殊写法被称为切点表达式,可以指定多个切点,其写法如下:
execution([修饰符] 返回值类型 包名.类名.方法名(参数类型) [异常])
在编写切点表达式的过程有几个点需要注意:
-
访问修饰符和异常可省略
-
返回值类型、包名、类名、方法名可以使用星号 * 来代表任意
-
包名与类名之间一个点,代表当前包下的类,两个点..表示当前包及其子包下的类
-
参数列表可以使用两个点..代表任意个数,任意类型的参数列表
下面是切点表达式的几个例子:
//指定方法
execution(public void com.water.aop.Target.method())
//任意方法
execution(void com.water.aop.Target.*)
//指定类下的任意方法(任意返回类型、任意参数)
execution(* com.water.aop.Target.*(..))
//指定包下的任意类的任意方法(任意返回类型、任意参数),最常用
execution(* void com.water.aop.*.*(..))
//指定包及其子包下的任意类的任意方法(任意返回类型、任意参数)
execution(* void com.water.aop..*.*(..))
//博君一笑
execution(* void com.water.aop..*.*(..))
测试
@RunWith(SpringJUnit4ClassRunner.class) //让测试在Spring容器环境下执行
@ContextConfiguration("classpath:applicationContext.xml") //加载配置文件
public class AppTest {
@Autowired //自动注入
private TargetInterface target;
@Test
public void test1() {
target.save();
}
}
基于注解的AOP开发
知道了如何通过XML进行AOP开发,我们过渡到注解开发就会变得相当容易了,不再需要各种复杂的配置,只需要简单记住几个注解的用法即可。
AOP的注解开发步骤
- 使用@Aspect标注切面类
- 使用@通知(增强)注解标注通知方法
- 在配置文件中配置组件扫描和AOP自动代理<aop:aspectj-autoproxy/>
- 测试
标注切面类和通知方法
为了让Spring知道哪些类是切面,我们会使用注解标明切面类:
@Component("myAspect") //使用注解配置Bean对象,切面类一般也配置为@Controller
@Aspect
- 注解通知(增强)的语法及类型:
@注解通知("切点表达式")
名称 | 注解 | 说明 |
---|---|---|
前置通知 | @Before | 指定的方法在切入点方法之前执行 |
后置通知 | @AfterReturning | 指定的方法在切入点方法之后执行 |
环绕通知 | @Around | 指定的方法在切入点方法之前和之后都执行 |
异常抛出通知 | @AfterThrowing | 指定的方法在切入点方法出现异常时执行 |
最终通知 | @After | 指定的方法无论切入点方法是否出现异常都执行 |
- 对切点表达式进行抽取
同XML配置AOP一样,我们也可以对切点表达式进行抽取。抽取方法是在切面内定义方法(空方法即可),在该方法上使用@Pointcut注解定义切点表达式,然后在增强注解中进行引用。
@Component("myAspect")
@Aspect
public class MyAspect {
@Before("MyAspect.myPoint()")
public void before() {
System.out.println("前置增强...");
}
@Pointcut("execution(* com.water.anno.*.*(..))")
public void myPoint() {}
}
配置组件扫描和AOP自动代理
<!-- 组件扫描 -->
<context:component-scan base-package="com.water.anno"/>
<!-- AOP自动代理 -->
<aop:aspectj-autoproxy/>