Spring与AOP思想
1. AOP简介
AOP(Aspect Orient Programming),面向切面编程,是面向对象编程OOP的一种补充。面向对象编程是从静态角度考虑程序的结构,而面向切面编程是从动态角度考虑程序运行过程。
通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术,AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型,利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
AOP底层,就是采用动态代理模式实现的。采用了两种代理:JDK的动态代理与CGLIB的动态代理。
面向切面编程,就是将交叉业务逻辑封装成切面,利用AOP容器的功能将切面织入到主业务逻辑中。所谓交叉业务逻辑是指,通用的、与主业务逻辑无关的代码,如安全检查、事务、日志等。
若不使用AOP,则会出现代码纠缠,即交叉业务逻辑与主业务逻辑混合在一起。这样,会使主业务逻辑变得混杂不清。
例如:转账,在真正转账业务逻辑前后,需要权限控制、日志记录、加载事务、结束事务等交叉业务逻辑,而这些业务逻辑与主业务逻辑间并无直接关系。但,它们的代码量所占必重能达到总代码两的一半甚至更多。它们的存在,不仅产生了大量的冗余代码,还大大干扰了主业务逻辑--转账。
总结:
- 利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
- AOP底层,就是采用动态代理模式实现的。采用了两种代理:JDK的动态代理与CGLIB的动态代理。
2. 为什么学习AOP
对程序进行增强:不修改源码的情况下,AOP可以进行权限校验,日志记录,性能监控,事务控制。
3. AOP的由来
AOP最早由AOP联盟的组织提出的,制定了一套规范,Spring将AOP思想引入到框架中,必须遵守AOP联盟的规范。
4. AOP的底层实现
代理机制:Spring的AOP底层用到两种代理机制。
- JDK的动态代理:针对实现了接口的类产生代理。
- CGLIB的动态代理:针对没有实现接口的类产生代理,应用的是底层的字节码增强的技术,生成当前的子类对象。
具体详解动态代理机制:
JDK动态代理【优先】
被代理对象必须要实现接口,才能产生代理对象.如果没有接口将不能使用动态代理技术。
JDK的动态代理,是使用反射技术获取类的加载器并且创建实例,根据类执行的方法,在执行方法的前后发送通知
在代理对象Proxy的新建代理实例方法中,必须要获得类的加载器,类所实现的接口,还有一个拦截方法的句柄。在句柄的invoke中如果不调用method.invoke则方法不会执行。在invoke前后添加通知,就是对原有类进行功能扩展了
创建好代理对象之后,proxy可以调用接口中定义的所有方法,因为它们实现了同一个接口,并且接口的方法实现类的加载器已经被反射框架获取到了
(1)java.lang.reflect.Proxy:该类用于动态生成代理类,只需传入目标接口的类加载器,目标接口数组以及InvocationHandler接口便可为目标接口生成代理类及代理对象
(2)java.lang.reflect.InvocationHandler:该接口包含一个invoke方法,通过该方法实现对委托类的代理的访问,是代理类完整逻辑的集中体现,包括要切入的增强逻辑和进行反射执行的真实业务逻辑
(3)java.lang.ClassLoader:类加载器类,负责将类的字节码装载到Java虚拟机中并为其定义类对象,然后该类才能被使用。Proxy静态方法生成动态代理类同样需要通过类加载器来进行加载才能使用,它与普通类的唯一区别就是其字节码是由JVM在运行时动态生成的而非预存在于任何一个.class 文件中
通过JDK的java.lang.reflect.Proxy类实现动态代理,会使用其静态方法newProxyInstance(),依据目标对象,业务接口及业务增强逻辑三者。自动生成一个动态代理对象
InvocationHadnler接口:实现了InvocationHandler接口的类用于加强目标类的主业务逻辑。这个接口中由一个方法invoke(),具体加强的代码逻辑就是定义在该方法中的,程序在调用主业务逻辑时,会自动调用invoke()方法。由于该方法是由代理对象自动调用的,所以这三个参数的值不用程序员给出
public interface UserService{
void save();
void delete();
void update();
void find();
}
public class UserServiceImpl implements UserService{
@Override
public void save(){System.out.println("保存用户!")};
@Override
public void delete(){System.out.println("删除用户!")};
@Override
public void update(){System.out.println("更新用户!")};
@Override
public void find(){System.out.println("查找用户!")};
}
public class UserServiceProxyFactory implments InvocationHandler{
// 需要代理的对象类,并通过构造方法对该对象进行初始化
private UserService us;
public UserServiceProxyFactory(UserService us){
super();
this.us = us;
}
public UserService get
public UserService getUserServiceProxy(){
// 生成动态代理
UserService usProxy = (UserService) Proxy.newProxyInstance(UserServiceProxyFactory.class.getClassLoader(),
UserService.class.gegtInterfaces(),this);
// 返回代理对象
return usProxy;
}
@Override
// 第一个参数是当前代理对象;第二个参数是当前调用的方法;第三个代理对象是:当前调用方法执行的参数数组
public Object invoke(Object proxy,Method method,Object[] args)throws Throwable(){
System.out.println("打开事务!");
Object invoke = method.invoke(us,args);
System.out.println("提交事务!");
return invoke;
}
}
public class Demo{
@Test
public void jdkProxyTest(){
UserService us = new UserServiceImpl();
UserServiceProxyFactor factory = new UserServiceProxyFactory(us);
UserService usProxy = factory.getUserServiceProxy();
usFactory.save();
}
}
CGLIB代理【没有接口】
第三方代理技术,cglib代理.可以对任何类生成代理.代理的原理是对目标对象进行继承代理. 如果目标对象被final修饰.那么该类无法被cglib代理。
JDK动态代理有一个限制,就是使用动态代理的对象必须实现一个或多个接口,如果想代理没有实现接口的类,就可以使用CGLIB实现。
使用JDK动态代理,要求目标类与代理类实现相同的接口,若目标类不存在接口,则无法使用该方式实现。但对于无接口的类,要为其创建动态代理,就要使用CGLIB来实现。
CGLIB代理的生成原理是通过生成目标类的子类,而子类是增强过的,这个子类对象就是代理对象。所以,使用CGLIB生成动态代理,要求目标类必须能够被继承,即不能是final的类。
CGLIB底层是通过使用一个小而快的字节码处理框架ASM来转换字节码并生成新的类。CGLIB是通过对字节码进行增强来生成代理的。
// MethodInterceptor是Callback的一个子接口
public class UserServiceProxyFactory implements MethodInterceptor{
public UserService getUserServiceProxy(){
// Enhancer:Spring帮我们集成好了,不需要自己找包,它会帮我们生成代理对象
Enhancer en = new Enhancer();
// 设置对谁进行代理
en.setSuperclass(UserServiceImpl.class);
// 代理要做什么
en.setCallback(this);
// 创建代理对象
UserService us = en.create();
return us;
}
@Override
// proxy:被代理的原始对象;Method:被代理对象的方法;args:运行的参数;methodProxy:代理方法传递过来的
public Object intercept(Object proxy,Method method,Object[] args MethodProxy methodProxy)throws Throwable(){
System.out.println("打开事务!");
Object invoke = methodProxy.invokeSuper(proxy,args);
System.out.println("提交事务!");
return invoke;
}
}
public class Demo{
@Test
public void cglibProxyTest(){
UserServiceProxyFactor factory = new UserServiceProxyFactory();
UserService usProxy = factory.getUserServiceProxy();
usFactory.save();
// 判断代理对象是否属于被代理对象类型
System.out.println(usProxy instanceof UserServiceImpl);
}
}
5. Spring中的AOP【AspectJ】
AOP思想最早由AOP联盟组织提出的。而Spring是使用这种思想最好的框架。
Spring的AOP最开始有自己实现的方式,但方式很繁琐。后来出现了AspectJ【是一个AOP框架】,Spring引入Aspect J作为自身AOP的开发。
Spring有两套AOP的开发方式:(1)Spring的传统方式【弃用】(2)Spring基于AspectJ的AOP开发(使用)
6. AOP编程术语
- Joinpoint【连接点】:所谓连接点是指那些被拦截到的点。在 spring 中,这些点指的是方法,因为 spring 只 支持方法类型的连接点。
- Pointcut【切入点】:所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义。
- Advice【通知/增强】:所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知.通知分为前置通知,后置 通知,异常通知,最终通知,环绕通知(切面要完成的功能)。
- Target【目标对象】:代理的目标对象
- Weaving【织入】:是指把增强应用到目标对象来创建新的代理对象的过程。Spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装在期织入。
- Proxy【代理】::一个类被 AOP 织入增强后,就产生一个结果代理类。
- Aspect【切面】:是切入点和通知(引介)的结合。
- Introduction【引介】:引介是一种特殊的通知在不修改类代码的前提下, Introduction 可以在运行期为类 动态地添加一些方法或 Field。
6.Spring中的AOP演示
基于XML的配置
(1)导包【4+2+2+2】:
spring的AOP包【2个】:spring-aspects-4.2.4.RELEASE.jar、spring-aop-4.2.4.RELEASE.jar;
Spring需要的第三方AOP包【2个】:com.springsource.org.aopalliance-1.0.0.jar、com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
AOP是由AOP联盟提出的一种编程思想,提出的是一套规范。而Spring是AOP这套规范的一种实现。所以,需要导入AOP联盟的规范接口包及Spring对其的实现包。
并在配置文件中添加beans的约束。
(2)准备定义目标对象
定义目标类,就是i当以之前的普通Bean类,也就是即将被增强的Bean类。
public class UserServiceImpl implements UserService{
@Override
public void save(){System.out.println("保存用户!")};
@Override
public void delete(){System.out.println("删除用户!")};
@Override
public void update(){System.out.println("更新用户!")};
@Override
public void find(){System.out.println("查找用户!")};
}
(3)准备通知类
通知类是指,实现了相应通知类型接口的类。当前,实现了这些接口,就要实现这些接口中的方法,而这些方法的执行,则是根据不同类型的通知,其执行时机不同。
通知:是切面的一种实现,可以完成简单织入功能(织入功能就是在这里完成的)。常用通知有:前置通知、后置通知、环绕通知、异常处理通知。
// 通知类
public class MyAdvice{
// 前置通知@Before:在目标方法运行之前调用
// 后置通知@AfterReturning:在目标方法运行之后调用【如果出现异常不会调用】
// 环绕通知@Around:在目标方法之前和之后都调用
// 异常通知@AfterThrowing:如果出现异常,就会调用
// 最终通知@After:在目标方法运行之后调用【无论是否出现异常都会调用】
public void before(){
System.out.println("这是前置通知!");
}
public void aferReturning(){
System.out.println("这是后置通知(如果不出现异常不会调用)");
}
public void around(ProceedingJoinPoint pjp) throw Throwable{
System.out.println("这是环绕通知之前的部分!!");
Object obj = pjp.proceed();// 调用目标方法
System.out.println("这是环绕通知之前的部分!!");
return obj;
}
public void afterException(){
System.out.println("出事啦!出现异常啦!!");
}
public void after(){
System.out.println("这是最终通知!(出现异常也会调用)");
}
}
(4)配置进行织入,将通知织入目标对象中
<!-- 准备工作: 导入aop(约束)命名空间 -->
<!-- 1.配置目标对象 -->
<bean name="userService" class="cn.itcast.service.UserServiceImpl" ></bean>
<!-- 2.配置通知对象 -->
<bean name="myAdvice" class="cn.itcast.d_springaop.MyAdvice" ></bean>
<!-- 3.配置将通知织入目标对象 -->
<aop:config>
<!-- 配置切入点
public void cn.itcast.service.UserServiceImpl.save()
void cn.itcast.service.UserServiceImpl.save()
* cn.itcast.service.UserServiceImpl.save()
* cn.itcast.service.UserServiceImpl.*()
* cn.itcast.service.*ServiceImpl.*(..)
* cn.itcast.service..*ServiceImpl.*(..)
-->
<!--3.1配置切点:-->
<aop:pointcut expression="execution(* cn.itcast.service.*ServiceImpl.*(..))" id="pc"/>
<!--3.2配置切面:-->
<aop:aspect ref="myAdvice" >
<!-- 指定名为before方法作为前置通知 -->
<aop:before method="before" pointcut-ref="pc" />
<!-- 后置 -->
<aop:after-returning method="afterReturning" pointcut-ref="pc" />
<!-- 环绕通知 -->
<aop:around method="around" pointcut-ref="pc" />
<!-- 异常拦截通知 -->
<aop:after-throwing method="afterException" pointcut-ref="pc"/>
<!-- 后置 -->
<aop:after method="after" pointcut-ref="pc"/>
</aop:aspect>
</aop:config>
配置文件中,除了要定义目标类和切面的Bean外,最主要的是在<aop:config/>
中进行aop的配置,而该标签的底层,会根据其子标签的配置,生成自动代理。
通过其子标签<aop:pointcut/>
定义切入点:该标签有两个属性,id和expression。分别用于指定该切入点的名称及切入点的值,expression的值为execution表达式。
通过子标签aop:aspect/定义具体的织入规则:根据不同的通知类型,确定不同的织入时间;将method指定的增强方法,按照指定织入时间,织入到切入点指定的目标方法中。
<aop:aspect/>
的ref属性用于指定使用哪个切面。
<aop:aspect/>
的子标签是各种不同的通知类型。不同的通知所包含的属性是不同的,但也有共同的属性。
- method:指定该通知使用的切面中的增强方法。
- pointcut-ref:指定该通知要应用的切入点。
AspectJ的6中通知的XML标签如下:
<aop:before/>
:前置通知
<aop:after-returning/>
:后置通知
<aop:after-around/>
:环绕通知
<aop:after-throwing/>
:异常通知
<aop:after/>
:最终通知
<aop:declare-parents/>
:引入通知
基于注解的配置
(1)导包【4+2+2+2】
(2)注册AspectJ的自动代理
在定义好切面AspectJ后,需要通知Spring容器,让容器生成”目标类+切面“的代理对象。这个代理是由容器自动生成的。只需要在Spring配置文件中注册一个基于AspectJ的自动代理生成器,其就会自动扫描到@AspectJ注解,并按通知类型与切入点,将其织入,并生成代理。
(3)准备目标对象
(4)准备通知
(5)配置进行织入,将通知织入到目标对象中
<!--1. 配置目标对象-->
<bean name="userService" class="com.zhy.UserServiceImpl"/>
<!--2. 配置通知对象-->
<bean name="myAdvice" class="com.zhy.MyAdvice"/>
<!--3. 开启使用注解完成织入-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
7. 通知类中的注解
- @Aspect:表示该类是一个通知类
- @Before("切点表达式"):指定该方法是前置通知,并指定切入点
在目标方法执行之前执行。被注解为前置通知的方法,可以包含一个JointPoint类型参数。该类型的对象本身就是切入点表达式。通过该参数,可获取切入点表达式、方法签名、目标对象等。
不光前置通知的方法,可以包含一个JointPoint类型参数,所有的通知方法均可包含该参数。
@Aspect
public class MyAspect{
@Before("execution(* *.. UserServiceImpl.doSome())")
public void beforeSome(){
System.out.println("前置增强");
}
@Before
public void beforeOther(JointPoint jp){
System.out.println("前置增强(切点表达式为)"+jp);
System.out.println("前置增强(方法签名为)"+jp.getSignature());
System.out.println("前置增强(目标对象为)"+jp.getTarget());
Object[] args = jp.getArgs();
if(args.length != 0){
System.out.println("前置增强(方法参数为):");
for(Object object : args){
System.out.println(object+" ");
}
}
}
}
- @AfterReturning("切点表达式"):指定该方法是后置通知,并指定切入点
在目标方法执行之后执行,由于是目标方法之后执行,所以可以获取到目标方法的返回值。该注解的returning属性就是用于指定接受方法返回值的变量名的。所以,被注解为后置通知的方法,除了可以包含JoinPoint参数外,还可以包含用于接受返回值的变量。该变量最好为Object类型,因为目标方法的返回值可能是任何类型。
@AfterReturning(value="execution(* *..UserServiceImpl.doThird())",returning="result")
public void afterReturning(Object result){
System.out.println("前置增强,目标方法执行结果为:"+result);
}
- @Around("切点表达式"):指定该方法是环绕通知,并指定切入点
在目标方法执行之前之后执行。被注解为环绕增强的方法要有返回值,Object类型。并且方法可以包含一个ProceedingJoinPoint类型的参数。接口ProceedingJoinPoint其有一个proceed()方法,用于执行目标方法。若目标方法有返回值,则该方法的返回值就是目标方法的返回值。最后,环绕增强方法将其返回值返回。该增强方法实际是拦截了目标方法的执行。
@Around("execution(* *..UserServiceImpl.doThird())")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
System.out.println(环绕通知:前");
Object result = pjp.proceed()l
System.out.println(环绕通知:后");
return result;
}
- @AfterThrowing("切点表达式"):指定该方法是异常通知,并指定切入点
在目标方法抛出异常后执行,该注解的throwing属性用于指定所发生的异常类对象。当然,被注解为异常通知的方法可以包含一个参数Throwable,参数名称为throwing指定的名称,表示发生的异常对象。
@AfterThrowing(value="execution(* *..UserServiceImpl.doThird())",throwing="e")
public void afterThrowing(Throwable e){
System.out.println("异常通知:"+e.getMessage());
}
- @After("切点表达式"):指定该方法是最终通知,并指定切入点
- @Pointcut("切点表达式"):让别的通知引用这个共有的切点表达式
当较多的通知增强方法使用相同的execution切入点表达式时,编写、维护均较为麻烦。AspectJ提供了@Pointcut注解,用于定义execution切入点表达式。
其用法是,将@Pointcut注解在一个方法之上,以后所有的execution的value属性值均可使用该方法名作为切入点,代表的就是@Pointcut定义的切入点。这个使用@Pointcut注解的方法一般使用private的标识方法,即没有实际作用的方法。
@After(value="mypointcut()")
public void after(){}
@Pointcut("execution(* *..UserServiceImpl.doThird())")
public void mypointcut() // 标识方法
public class MyAdvice{
@Pointcut("execution(* com.zhy.service.*ServiceImpl.*(..))")
public void pc(){}
@Before("MyAdvice.pc()")
public void before(){}
}
8. AOP中的切点表达式
切点表达式的语法:
基于execution函数完成的:[访问修饰符] 方法返回值 包名.类名.方法名(参数)
案例
- 指定切入点:任意公共方法:
execution(public * *(..))
- 任意一个以set开始的方法:
execution(* set *(..))
- 指定service包里的任意类的任意方法:
execution(* com.zhy.service.*.*(..))
- 定义在service包及其子包中的任意类的任意方法:
execution(* com.zhy.service..*.*(..))
- 接口中及其实现类中的任意方法。若为类,则为类及其子类中的任意方法:
execution(* com.zhy.service.IService+.*(..))
- 所有joke(String,int)方法为切入点。如果方法中的参数类型是java.lang包下的类,则可以直接使用类名;如果不是则必须使用全限定类名如joke(java.util.List,int):
execution(* joke(String,int))
- 第二个参数任意类型,但只能有2个参数:
execution(* joke(String,*))
- 可变参数类型:
execution(* joke(String,..))
- 第一个参数只能是Object类型:
execution(* joke(Object))
- 第一个参数可以是任意对象类型:
execution(* joke(Object+))