AOP 即 Aspect Oriented Program 面向切面编程
首先,在面向切面编程的思想里面,把功能分为核心业务功能,和周边功能。
所谓的核心业务,比如登陆,增加数据,删除数据都叫核心业务
所谓的周边功能,比如性能统计,日志,事务管理等等
周边功能在Spring的面向切面编程AOP思想里,即被定义为切面
在面向切面编程AOP的思想里面,核心业务功能和切面功能分别独立进行开发
然后把切面和核心业务功能 "编织" 在一起,这就叫AOP
名词概念
- JoinPoint 连接点,程序运行中的某个(被拦截的)阶段点
- PointCut 切点。Pointcut是JoinPoint的集合,即一个或多个方法被拦截点的集合
- Aspect 切面。和核心业务功能编织在一起的周边功能的模块。
- Advice Aspect中应用在切入点中的代码或者说方法,如,切面类中被@before(....)等注释了的成员函数
注意:Spring会根据是否实现了接口自动切换JDK动态代理和CGLib动态代理
1.如果是使用Jdk动态代理实现Spring AOP,Spring容器的getBean方法获得的对象是不能转型成该Bean定义的Class类型。
假设有Service接口和实现该接口的ServiceImpl类,使用Jdk动态代理实现Spring AOP,getBean获得的对象是Jdk动态代理生成的代理类的对象,这个代理类只是实现了Service接口,而没有继承ServiceImpl。 使用CGLib动态代理不会有这个问题,因为CGLib动态代理生成的代理类是继承我们的目标类的,而不是实现目标接口。
2.如果使用CGLib动态代理实现Spring AOP,通过Spring容器的getBean方法获得的对象不能直接引用目标类的公有属性,读取或者修改公有属性。
假设Service类没有实现任何接口,使用Spring容器的getBean方法时转型成Service类的对象service,但我们不能调用service.type来获得或者修改type属性。
这是因为使用CGLib动态代理实现的Spring AOP,调用service.type是引用CGLib代理类对象的属性,而不是目标对象的type属性。
总之:如果实现了接口,getBean要强转成接口,如果没有实现接口,则可以转成实现类。
注解方式
首先了解一下几种注解的作用:
@Aspect 将某个类声明成一个切面
@Pointcut("切入点表达式") 声明一个切点,我们可利用方法签名来编写切入点表达式。最典型的切入点表达式是根据方法的签名来匹配各种方法:
- execution (* cn.itcast.service..*.*(..)) 匹配cn.itcast.service包及其所有子包下的任何返回值的任何方法
- execution (* cn.itcast.service.impl.PersonServiceImpl.*(..)):匹配PersonServiceImpl类中声明的所有方法。第一个代表任意修饰符及任意返回值类型,第二个代表任意方法,..匹配任意数量任意类型的参数,若目标类与该切面在同一个包中,可以省略包名。
- execution public * cn.itcast.service.impl.PersonServiceImpl.*(..):匹配PersonServiceImpl类中的所有公有方法。
- execution public double cn.itcast.service.impl.PersonServiceImpl.*(..):匹配PersonServiceImpl类中返回值类型为double类型的所有公有方法。
- execution public double cn.itcast.service.impl.PersonServiceImpl.*(double, ..):匹配PersonServiceImpl类中第一个参数为double类型,后面不管有无参数的所有公有方法,并且该方法的返回值类型为double类型。
- execution public double cn.itcast.service.impl.PersonServiceImpl.*(double, double):匹配PersonServiceImpl类中参数类型为double,double类型的,并且返回值类型也为double类型的所有公有方法。
@Before("切入点表达式或已经声明的切入点")前置通知方法。 前置通知方法在目标方法开始之前执行。
@AfterReturning("")返回前执行
@AfterThrowing("") 抛出异常前执行
@After("") 后置通知。方法执行完后一定会执行。类似finaly
@Around("") 环绕通知。被拦截(被切入)方法的调用由环绕通知决定。切面方法需要传入一个ProceedingJointPoint对象,该对象用于启用被拦截方法。实例如下:
public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("start log:" + joinPoint.getSignature().getName());//前置
Object object = joinPoint.proceed();//启用被拦截的核心业务方法
System.out.println("end log:" + joinPoint.getSignature().getName());//后置
return object;
}
单个Aspect情况:
多个Aspect情况:
为了便于理解,还可以参考如下的图:
将切面想象成同心圆,@Order(n)中n越小圆越大,越先执行。
而先执行的后退出。
示例程序:
aspect:
package com.myspring.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component//这里务必记得要把aspect也加入到容器里
public class Log{
//实际中尽量不要复用切入点表达式,而是声明一个切入点方法
@After("execution(* com.myspring.imple..*(..))")
public void after() {
System.out.println("after");
}
@Around("execution(* com.myspring.imple..*(..))")
public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("start log:" + joinPoint.getSignature().getName());
Object object = joinPoint.proceed();
System.out.println("end log:" + joinPoint.getSignature().getName());
return object;
}
@Before( "execution(* com.myspring.imple..*(..))")
public void before() {
System.out.println("before");
}
}
service:
package com.myspring.imple;
import org.springframework.stereotype.Component;
@Component("s")
public class ServiceImpl
{
public void save()
{
System.out.println("save run");
}
}
applicationContext.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
<!--添加aop命名空间-->
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<context:component-scan base-package="com.myspring.imple"/>
<context:component-scan base-package="com.myspring.aspect"/>
<!-- spring调用了aspectj——一个面向切面的框架的自动代理 -->
<aop:aspectj-autoproxy/>
</beans>
test:
package com.myspring.test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.myspring.imple.ServiceImpl;
public class test
{
public static void main(String[] args)
{
// TODO 自动生成的方法存根
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
//如果核心业务实现了接口则为JDK动态代理,需要强转为接口,否则为CGLib动态代理,强转为实现类
ServiceImpl imp1 = (ServiceImpl)context.getBean("s");
imp1.save();
}
}
配置文件方式
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<!--首先将核心功能和周边功能的Bean加载到容器里-->
<bean id="logAspect" class="com.how2java.aspect.LoggerAspect"></bean>
<bean id="service" class="com.how2java.service.ProductService"></bean>
<aop:config>
<!-- 声明一个切入点 -->
<aop:pointcut expression="execution(* com.how2java.service.ProductService.*(..))" id="logPointcut"/>
<!-- 声明切面 -->
<aop:aspect ref="logAspect" id="log" >
<!-- 设定aspect中advice的方法与绑定的切入点 -->
<!--使用已经定义好的pointcut-->
<aop:around method="around" pointcut-ref="logPointcut"/>
<aop:before method="before" pointcut-ref="logPointcut"/>
<!--使用切入点表达式定义Pointcut-->
<aop:after method="after" pointcut="execution(* com.how2java.service.ProductService.*(..))"/>
</aop:aspect>
</aop:config>
</beans>
名字太多记不住?我们来理一遍。
首先加载两个bean:
作为切面的bean——logAspect以及作为核心功能的bean——service。对应注解模式的@Component
然后开始配置aop:
拦截住核心功能的一个方法(声明一个pointcut),名字叫logPointcut,对应注解@Pointcut
接着将作为切面的bean——logAspect声明为一个切面,名字是log,对应注解@Aspect
现在切入点和切面都有了,最后将他们编织在一起:
在切面中设置advice类型和周边功能方法,并且与相应的切入点——logPointcut绑定,对应注解@Before(切入点表达式或已定义的切入点)等
ps:Pointcut定义在Aspect外则所有Aspect都可以捕获,定义在某个Aspect里则只有该Aspect内的Advice可以捕获
当我们使用别人写好的周边功能与我们写的核心业务编织时,应当使用配置文件方式;当我们独立完成并且封装整个系统时才考虑使用注解方式。因此推荐使用配置文件方式。