Spring的AOP实现原理
动态代理
JDK
动态代理 只能对实现了接口的类产生代理Cglib
动态代理 类似于javassist第三方代理技术,对没有实现接口的类产生代理对象,生成子类对象
<!--more-->
相关术语
class UserDao{
public void save(){};
public void find(){};
public void delete(){}
}
JoinPoint
: 连接点,也就是可以被连接的的的点,比如上面代码的几个方法都是可以被增强的,这些方法也就是叫做连接点。Pointcut
:切入点,也就是真正被拦截的点,上面的连接点可以说是理论上,而pointcut
就是则是实际中的,比如对save
方法进行增强那么save
就可以被称为切入点。advice
: 通知,也可以说是增强,比如现在在save
方法之前需要进行权限校验,那么权限校验的那个方法就是通知。target
:被增强的对象,比如上面代码中是对UserDao
进行增强,因此UserDao
就是目标。weaving
:织入,将通知(advice
)应用到(target
)的过程就是织入aspect
:切面,多个通知和多个切入点结合就是切面。
入门案例
所必需jar包
aopalliance-1.0 spring的aop只是实现了别人的定好的规范,所以需要这个包
aspectjweaver-1.8.0 spring-aop通过aspectj来实现的,所以需要这个包
spring-aop
spring-aspects spring与aspectj整合的包
spring-expression
spring-beans
spring-context
spring-core
配置文件
约束文件
spring-framework-4.3.9.RELEASE\docs\spring-framework-reference\html\xsd-configuration.html文件里面的aop schema那部分
spring整合Junit
此时还需要添加一个jar
包:spring-test
案例代码
接口
package top.twolovelypig.demo;
public interface UserDao {
public void save();
public void find();
public void delete();
public void update();
}
实现类
package top.twolovelypig.demo;
public class UserDaoImpl implements UserDao {
@Override
public void save() {
System.out.println("保存");
}
//省略了另外几个方法
}
配置文件
<?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" xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="userDao" class="top.twolovelypig.demo.UserDaoImpl"></bean>
</beans>
测试代码
package top.twolovelypig.demo;
import javax.annotation.Resource;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class TestDemo {
@Resource(name="userDao")
private UserDao UserDao;
@Test
public void test() {
UserDao.delete();
}
}
平时想要获取bean需要从context容器中获取,但是对于使用了junit测试可以使用注解来完成这个一步,注意点如下:
- 在类上面需要使用
@RunWith(SpringJUnit4ClassRunner.class)
,这是固定的 @ContextConfiguration("classpath:applicationContext.xml")
这个注解是用来加载配置文件的,上面的案例中的配置文件是在classpath
下(src
目录下)。- 在类上面使用了上面两个注解之后可以在对象上可以使用
@Resource
注解即可使用该对象。
通过xml形式配置aop
切面代码
package top.twolovelypig.demo;
public class MyAspect {
public void check() {
System.out.println("校验");
}
}
此时的配置文件
<?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" xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="userDao" class="top.twolovelypig.demo.UserDaoImpl"></bean>
<!-- 将切面类交给spring管理 -->
<bean id="myAspect" class="top.twolovelypig.demo.MyAspect"></bean>
<!--配置切面:通过aop的配置完成对目标类完成代理 -->
<aop:config>
<!--切入点的配置,id是任意值,表达式配置的是拿些类那些方法需要增强 -->
<aop:pointcut expression="execution(* top.twolovelypig.demo.UserDaoImpl.save(..))" id="checkSave"/>
<!-- 切面的配置: ref中的值是配置的切面类的bean的id -->
<aop:aspect ref="myAspect">
<!-- 通知的配置,method是配置的切面类里面的方法,pointcut-ref里面的值是切点的id -->
<aop:before method="check" pointcut-ref="checkSave"/>
</aop:aspect>
</aop:config>
</beans>
此时在测试代码岁执行userDao.save()
时就会先执行切面类的check()
方法。
spring通知类型
前置通知
- 可以获得切入点的信息(在切面的方法里面传入
JoinPoint
参数即可) - 上面的案例中就是使用的前置通知
(aop:before)
后置通知
- 可以获取切入点的信息
- 可以获取方法的返回值
具体配置如下
配置文件
<?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" xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="userDao" class="top.twolovelypig.demo.UserDaoImpl"></bean>
<!-- 将切面类交给spring管理 -->
<bean id="myAspect" class="top.twolovelypig.demo.MyAspect"></bean>
<!--配置切面:通过aop的配置完成对目标类完成代理 -->
<aop:config>
<!--切入点的配置,id是任意值,表达式配置的是拿些类那些方法需要增强 -->
<aop:pointcut expression="execution(* top.twolovelypig.demo.UserDaoImpl.save(..))" id="checkSave"/>
<!-- 配置后置通知的切点信息 -->
<aop:pointcut expression="execution(* top.twolovelypig.demo.UserDaoImpl.delete(..))" id="checkAfter"/>
<!-- 切面的配置: ref中的值是配置的切面类的bean的id -->
<aop:aspect ref="myAspect">
<!-- 通知的配置,method是配置的切面类里面的方法,pointcut-ref里面的值是切点的id -->
<aop:before method="check" pointcut-ref="checkSave"/>
<!-- 配置后置通知的切面使用的是after-returing而不是after -->
<aop:after-returning method="afterCheck" pointcut-ref="checkAfter" returning="result"/>
</aop:aspect>
</aop:config>
</beans>
<font color="red">需要注意的是后置通知使用使用的是after-returing
而不是after
,在后置通知中返回值配置时通过returning=“rsult”
,这里的result
是可以随意写的一个字符串,不一定就是result
。</font>
切面类
package top.twolovelypig.demo;
public class MyAspect {
/**
* 前置通知的方法
*/
public void check() {
System.out.println("校验");
}
/**
* 后置通知的方法
* @param result
*/
public void afterCheck(Object result) {
System.out.println("后置通知");
System.out.println(result);
}
}
切面类里面的这个后置通知方法当中有一个参数是Objcet
类型,此值可以接受任意类型的返回值,不过需要注意的是形参在这里必须是result
,因为在配置文件当中配置的是result
,也就是说这两个位置的值必须是一致的。
环绕通知
环绕通知可以控制切点方法的执行与否,因为环绕通知在切点方法前后都需要执行,所以能够控制切点方法是否执行。
配置文件信息
<!--配置切面:通过aop的配置完成对目标类完成代理 -->
<aop:config>
<!--切入点的配置,id是任意值,表达式配置的是拿些类那些方法需要增强 -->
<aop:pointcut expression="execution(* top.twolovelypig.demo.UserDaoImpl.save(..))" id="checkSave"/>
<!-- 配置后置通知的切点信息 -->
<aop:pointcut expression="execution(* top.twolovelypig.demo.UserDaoImpl.delete(..))" id="checkAfter"/>
<!-- 配置环绕通知的切点信息 -->
<aop:pointcut expression="execution(* top.twolovelypig.demo.UserDaoImpl.update(..))" id="checkAround"/>
<!-- 切面的配置: ref中的值是配置的切面类的bean的id -->
<aop:aspect ref="myAspect">
<!-- 通知的配置,method是配置的切面类里面的方法,pointcut-ref里面的值是切点的id -->
<aop:before method="check" pointcut-ref="checkSave"/>
<!-- 配置后置通知的切面使用的是after-returing而不是after -->
<aop:after-returning method="afterCheck" pointcut-ref="checkAfter" returning="result"/>
<!-- 配置环绕通知 -->
<aop:around method="arround" pointcut-ref="checkAround"/>
</aop:aspect>
</aop:config>
切面代码
/**
* 环绕通知
* @param proceedingJoinPoint
* @throws Throwable
*/
public Object arround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("前置通知");
//这一句就是用来控制目标程序的执行与否,在此之前执行的相当于是前置通知,在此之后执行的是后置通知,目标程序可能有返回值,所以需要使用Object来接收
Object object = proceedingJoinPoint.proceed();
System.out.println("后置通知");
return object;
}
异常抛出通知
配置文件
<!--配置切面:通过aop的配置完成对目标类完成代理 -->
<aop:config>
<!--切入点的配置,id是任意值,表达式配置的是拿些类那些方法需要增强 -->
<aop:pointcut expression="execution(* top.twolovelypig.demo.UserDaoImpl.save(..))" id="checkSave"/>
<!-- 配置后置通知的切点信息 -->
<aop:pointcut expression="execution(* top.twolovelypig.demo.UserDaoImpl.delete(..))" id="checkAfter"/>
<!-- 配置环绕通知的切点信息 -->
<aop:pointcut expression="execution(* top.twolovelypig.demo.UserDaoImpl.update(..))" id="checkAround"/>
<!-- 配置异常通知的切点信息 -->
<aop:pointcut expression="execution(* top.twolovelypig.demo.UserDaoImpl.find(..))" id="checkException"/>
<!-- 切面的配置: ref中的值是配置的切面类的bean的id -->
<aop:aspect ref="myAspect">
<!-- 通知的配置,method是配置的切面类里面的方法,pointcut-ref里面的值是切点的id -->
<aop:before method="check" pointcut-ref="checkSave"/>
<!-- 配置后置通知的切面使用的是after-returing而不是after -->
<aop:after-returning method="afterCheck" pointcut-ref="checkAfter" returning="result"/>
<!-- 配置环绕通知 -->
<aop:around method="arround" pointcut-ref="checkAround"/>
<!-- 配置异常通知,异常通知是可以获取异常的信息的 -->
<aop:after-throwing method="afterThrowing" pointcut-ref="checkException" throwing="ex"/>
</aop:aspect>
</aop:config>
切面代码
/**
* 异常通知
* @param ex
*/
public void afterThrowing(Throwable ex) {
System.out.println("异常通知"+ ex.getMessage());
}
在上面的配置文件当中配置了一个throwing=“ex”
,这个参数是为了获取异常的信息来配置的,至于里面的ex
是可以随意取的,但是必须与切面类里面的形参保持一致。
最终通知
无论代码是否有异常,里面的代码总是会执行,最终通知配置的时候使用的是after
。
spring的切入点表达式的写法
语法格式
- 基于execution的函数完成的
- [访问修饰符] 方法返回值 包名.类名.方法名(参数)
public void top.twolovelypig.UserDao.save(..)
最后面方法里面的两个点表示是任意形参* *.*.*.Dao.save(..)
这里表示的是任意返回值,要三层包,类名以Dao
结尾的所有save()
方法(方法参数任意),也就是execution表达式是可以使用通配符的。
spring-aop注解形式
spring的基于aspectJ的注解形式的aop开发
引入的jar包
使用spring基本的6个jar包
beans包
expression包
core包
context包
common.logging日志包
log4j包
aop开发的jar包
aop包
aspectj包
aspect.weaver包
aopaliance包(定义aop规范的包)
配置文件
<?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:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- 在配置文件中开启注解的aop的开发 -->
<aop:aspectj-autoproxy />
<bean id="userDao" class="top.twolovelypig.demo.UserDao"></bean>
<bean id="myAspect" class="top.twolovelypig.demo.MyAspetAnno"></bean>
</beans>
配置文件头部依然是可以使用aop
的,有一个需要注意的点就是在配置文件中需要开启注解形式的aop
的开发。也就是这一句 <aop:aspectj-autoproxy />
操作类
业务类(没有接口)
<font color="red">这里的也是类之所以不创建接口时为了使用cglib
形式的代理,如果业务类是含有接口的那么就会使用jdk
动态代理</font>
package top.twolovelypig.demo;
public class UserDao {
public void save() {
System.out.println("保存数据");
}
public void delete() {
System.out.println("删除数据");
}
public void update() {
System.out.println("更新数据");
}
public void find() {
System.out.println("查找数据");
}
}
切面类
package top.twolovelypig.demo;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
/**
* 切面类
* @author lg
*/
@Aspect
public class MyAspetAnno {
@Before(value="execution(* top.twolovelypig.demo.UserDao.save(..))")
public void before() {
System.out.println("前置通知");
}
}
在切面类中需要注意以下几点:
-
类中需要使用
@Aspect
注解。该注解标识该类是一个切面类。 -
对于通知需要使用
@Before
(前置通知),@AfterReturning
(后置通知)等类标识通知类型,在这些注解后面的括号里面的value
写上execution
表达式,对于后置通知有返回值时可以按照如下方式来写:@AfterReturning(value="execution(* top.twolovelypig.demo.UserDao.update(..))", returning="result") public void afterReturning(Object result) { System.out.println("后置通知"+result); }
<font color="red">此时需要注意的是returning里面的值必须与后置通知方法里面的形参一致。</font>
对于异常通知可以获取异常信息,此时可以按照如下方式来写:
@AfterThrowing(value="execution(* top.twolovelypig.demo.UserDao.delete(..))", throwing="e") public void afterThrowing(Throwable e) { System.out.println("后置通知"+e.getMessage()); }
<font color="blue">此时需要注意的是注解里面的throwing=“e”这里的e必须与异常通知方法里面的形参名称保持一致</font>
对于环绕通知是可以掌握切入点方法的执行与否,具体配置如下:
@Around(value="execution(* top.twolovelypig.demo.UserDao.find(..))") public Object around(ProceedingJoinPoint joinPoint) { System.out.println("在切点前面执行的相当于是前置通知"); Object object = joinPoint.proceed();//有这一句就相当于是切点方法执行 System.out.println("在切点后面执行的相当于是后置通知"); return object; }
<font color="green">此时需要注意的是对于环绕通知方法是含有返回值Object的,因为环绕通知需要控制切入点方法的执行,切点方法可能有返回值,所以需要使用object类接收并返回</font>
测试类
package top.twolovelypig.demo;
import javax.annotation.Resource;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class TestDemo {
@Resource(name="userDao")
private UserDao userDao;
@Test
public void testAspectJ() {
userDao.save();
userDao.delete();
userDao.update();
userDao.find();
}
}
spring的aop注解切入点的配置
在使用注解配置切面方法的时候都是向下面这样的方式来配置的。
@Before(value="execution(* top.twolovelypig.demo.UserDao.save(..))")
public void before() {
System.out.println("前置通知");
}
如果切面很好还好,但是如果很多的话就比较麻烦,所以可以先将切点配置出来,然后在切面里面直接引用即可。具体配置如下:
package top.twolovelypig.demo;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
/**
* 切面类
* @author lg
*/
@Aspect
public class MyAspetAnno {
@Before(value="MyAspetAnno.pointCut1()")
public void before() {
System.out.println("前置通知");
}
@AfterReturning(value="MyAspetAnno.pointCut2()")
public void afterReturning() {
System.out.println("后置通知");
}
@Pointcut(value="execution(* top.twolovelypig.demo.UserDao.delete(..))")
private void pointCut1() {}
@Pointcut(value="execution(* top.twolovelypig.demo.UserDao.save(..))")
private void pointCut2() {}
}
可以看到此时切面方法里面配置的execution
可以提取出来单独配置,然后在切面方法里面去引用即可,至于引用方法则是类名.方法名。