1.专业术语
- Aspect(切面):指封装的用于横向插入系统功能(如事务、日志等的类),该类要被spring容器识别为切面,需要在配置文件中通过<bean>元素指定
- Joinpoint(连接点):指对象的一个操作,例如方法的调用或者异常的抛出,在SpringAOP中,连接点就是指方法的调用.
- Poincut(切入点):指的是类或方法名,如某个通知要应用到所有以add开头的方法中,那么所有满足这一规则的方法就是切入点
- Advice(通知/增强处理):在定义好的切入点处要执行的程序代码,可以将其理解为切面类中的方法,它是切面类中的具体体现.
- Target Object(目标对象):是指所有被通知的对象.
- Proxy(代理):将通知应用到目标对象之后,被动态创建的对象
- Weaving(织入):将切面代码插入到目标对象上,从而生成代理对象的过程
2.动态代理
2.1JDK动态代理
JDK动态代理是通过java.long.reflect.Proxy类来实现的,我们可以调用Proxy类的newProxyInstance()方法来创建代理对象
案例:
(1)UserDao
public interface UserDao{
public void addUser();
public void deleteUser();
}
(2)UserDaoImpl
//目标类
public class UserDaoImpl implements UserDao{
public void addUser(){
System.out.println("添加用户");
}
public void deleteUser(){
System.out.println("删除用户");
}
}
(3)MyAspect
//切面类:可以存在多个通知Advice(即增强的方法)
public class MyAspect{
public void check_Permissions(){
System.out.println("模拟检查权限");
}
public void log(){
System.out.println("模拟记录日志");
}
}
(4)创建代理类JdkProxy
//jdk代理类
public class JdkProxy implements InvocationHandler{
//声明目标类接口
private UserDao userDao;
//创建代理方法
public Object createProxy(UserDao userDao){
this.userDao=userDao;
//1.类加载器
ClassLoader classLoader=JdkProxy.class.getClassLoader();
//2.被代理对象实现的所有接口
Class[] clazz=userDao.getClass().getInterfaces();
//3.使用代理类,进行增强,返回的是代理后的对象
return Proxy.newProxyInstance(classLoader,clazz,this);
}
/*
所有动态代理类的方法调用都交给invoke()方法处理
proxy被代理后的对象
method将要被执行的方法信息(反射)
args执行方法时需要的参数
*/
@Override
public Object invoke(Object proxy,Method method,Object[]args)throws Throwable{
//声明切面
MyAspect myAspect=new MyAspect();
//前增强
myAspect.check_Permissions();
//在目标类上调用方法,并传入参数
Object obj=method.invoke(userDao,args);
//后增强
myAspect.log();
return obj;
}
}
(5)JdkTest
public class JdkTest{
public static void main (String []args){
//创建代理对象
JdkProxy jdkProxy=new JdkProxy();
//创建代理对象
JdkProxy jdkProxy=new JdkProxy();
//创建目标对象
UserDao userDao=new UserDaoImpl();
//从代理对象中获取增强后的目标对象
UserDao userDao1=(UserDao)jdkProxy.createProxy(userDao);
//执行方法
userDao1.addUser();
userDao1.deleteUser();
}
}
2.2CGLIB代理
如果要对没有实现接口的类实现代理,那么可以使用CGLIB代理
(1)UserDao
//目标类
public class UserDao {
public void addUser(){
System.out.println("添加用户");
}
public void deleteUser(){
System.out.println("删除用户");
}
}
(2)创建代理类CglibProxy
//代理类
public class CglibProxy implements MethodInterceptor{
//代理方法
public Object createProxy(Object target){
//创建一个动态类对象
Enhancer enhancer=new Enhancer();
//确定需要增强的类,设置其父类
enhancer.setSuperclass(target.getClass());
//添加回调函数
enhancer.setCallback(this);
//返回创建的代理类
return enhancer.create();
}
/*
proxy CGlib 根据指定父类生成的代理对象
method 拦截的方法
args拦截方法的参数数组
methodProxy 方法的代理对象,用于执行父类的方法
*/
@Override
public Object intercept(Object proxy,Method method,Object[]args,MethodProxy methodProxy)throws Throwable{
//创建切面类对象
MyAspect myAspect=new MyAspect();
//前增强
myAspect.check_Permissions();
//目标方法执行
Object obj=methodProxy.invokeSuper(proxy,args);
//后增强
myAspect.log();
return obj;
}
}
(3)CglibTest
//测试类
public class CglibTest{
public static void main (String []args){
//创建代理对象
CglibProxy cglibProxy=new CglibProxy();
//创建目标对象
UserDao userDao=new UserDao();
//获取增强后的目标对象
UserDao userDao1=(UserDao)cglibProxy.createProxy(userDao);
//执行方法
userDao1.addUser();
userDao1.deleteUser();
}
}
3.Spring的通知类型
MethodInterceptor(环绕通知) | 在目标方法执行前后实施增强,可以应用于日志、事务管理等功能 |
MethodBeforeAdvice(前置通知) | 在目标方法执行前实施增强,可以应用于权限管理等功能 |
AfterReturingAdvice(后置通知) | 在目标方法执行后实施增强,可以应用于关闭流、上传文件、删除临时文件等功能 |
ThrowsAdvice(异常通知) | 在方法抛出异常后实施增强,可以应用于处理异常记录日志等功能. |
IntroductionInterceptor(引介通知) | 增强类(在目标类中添加一些新的方法和属性,可以应用于修改老版本程序) |
4.ProxyFactoryBean
ProxyFactoryBean负责为其它Bean创建代理实例,在Spring中使用ProxyFactoryBean是创建AOP代理的基本方式
target | 代理的目标对象 |
proxyInterfaces |
代理要实现的接口,如果是多个接口,可以使用以下格式赋值 <list> <value></value> </list> |
proxyTargetClass | 是否对类代理而不是接口,设置为true时,使用CGLIB代理 |
interceptorNames | 需要织入目标的Advice |
singleton | 返回的代理是否为单实例,默认为true(即返回单实例) |
optimize | 当设置为true时,强制使用CGLIB |
接下来通过一个典型的环绕通知案例,来掩饰spring使用ProxyFactoryBean创建AOP代理的过程
(1)factorybean包,在该包中创建切面类MyAspect
//切面类
public class MyAspect implements MethodInterceptor{
@Override
public Object invoke(MethodInvocation mi) throws Throwable{
check_Permissions();
//执行目标方法
Object obj=mi.proceed();
log();
return obj;
}
public void check_Permissions(){
System.out.println("模拟检查权限");
}
public void log(){
System.out.println("模拟记录日志");
}
}
(2)applicationContext.xml中指定代理对象
<!--1 目标类-->
<bean id="userDao" class="com.itcast.jdk.UserDaoImpl"/>
<!--2切面类-->
<bean id="userDao" class="com.itcast.factorybean.MyAspect"/>
<!--3使用spring代理工厂定义一个名称为userDaoProxy的代理对象-->
<bean id="userDaoProxy" class="org.springframework.aop.framework.proxyFactoryBean"/>
<!--3.1指定代理实现的接口-->
<property name="proxyInterfaces" value="com.itcast.jdk.UserDao"/>
<!--3.2 指定目标对象-->
<proxy name="target" ref="userDao"/>
<!--3.3 指定切面,植入环绕通知-->
<property name="interceptorNames" value="myAspect"/>
<!--3.4 指定代理方式,true:使用cglib,false(默认):使用jdk动态代理-->
<property name="proxyTargetClass" value="true"/>
</bean>
(3)ProxyFactoryBeanTest
//测试类
public class ProxyFactoryBeanTest{
public static void main(String[]args){
String xmlpath="com/itcast/factorybean/applicationContext.xml";
ApplicationContext applicationContext=new ClassPathXmlApplicationContext(xmlpath);
//从spring容器获得内容
UserDao userDao=(UserDao) applicationContext.getBean("userDaoProxy");
//执行方法
userDao.addUser();
userDao.deleteUser();
}
}
5.AspectJ开发
5.1基于XML的声明式AspectJ
1.配置切面
在spring的配置文件中,配置切面使用的是<aop:aspect>元素,该元素会将一个已定义好的StringBean转换成切面Bean,所以要在配置文件中定义一个普通的SpringBean.定义完成后,通过<aop:aspect>元素的ref属性即可引用该Bean
配置<aop:aspect>元素时,通常会指定id和ref两个属性
属性名称 | 描述 |
id | 用于定义该切面的唯一标识名称 |
ref | 用于引用普通的SpringBean |
2.配置切入点
当<aop:pointcut>元素作为<aop:config>元素的子元素定义时,表示该切入点是全局切入点,它可被多个切面所共享,当<aop:pointcut>元素作为<aop:aspect>元素的子元素时,表示该切入点只对当前切面有效.
属性名称 | 描述 |
id | 用于指定切入点的唯一标识名称 |
expression | 用于指定切入点关联的切入点表达式 |
切入点表达式的基本格式如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)throws-pattern?)
modifiers-pattern | 表示定义的目标方法的访问修饰符,如public、private等 |
ret-type-pattern | 表示定义的目标方法的返回值类型,如void、String等 |
declaring-type-pattern | 表示定义的目标方法的类路径,例如com.itcast.userDaoImpl |
name-pattern | 表示具体需要被代理的目标方法,如add() |
param-pattern | 表示需要被代理的目标方法包含的参数 |
throws-pattern | 表示需要被代理的目标方法抛出的异常类型 |
可配置项 | modifiers-pattern、declaring-type-pattern、throws-pattern |
execution(* com.itcast.jdk.*.*(..))
execution | 表达式的主体 |
第1个* | 表示返回类型,使用*代表所有类型 |
com.itcast.jdk | 表示需要拦截的包名 |
第2个* | 表示的是类名,使用*代表所有的类 |
第3个* | 表示的是方法名,使用*表示的是所有方法 |
(..) | 表示方法的参数,其中..表示任意参数 |
注意: | 第一个*与包名之间有一个空格 |
3.配置通知
<aop:aspect>的子元素配置了5中常用通知,这5个子元素不支持使用子元素,但在使用时可以指定一些属性
pointcut | 该属性用于指定一个切入点表达式,Spring将在匹配该表达式的连接点时织入该通知 |
pointcut-ref |
该属性指定一个已经存在的切入点名称,通常pointcut 和pointcut-ref两个属性只需要使用其中之一 |
method | 该属性制定一个方法名,指定将切面Bean中的该方法转换为增强处理 |
throwing | 该属性只对<after-throwing>元素有效,它用于指定一个形参名,异常通知方法可以通过该形参访问目标方法所抛出的异常 |
returning | 该属性只对<after-returning>元素有效,它用于指定一个形参名,后置通知方法可以通过该形参访问目标方法的返回值 |
通过一个案例演示如何在Spring中通过基于xml的声明式AspectJ
(1)MyAspect切面类
//切面类,在此类中编写通知
public class MyAspect{
//前置通知
public void myBefore(JoinPoint joinpoint){
System.out.println("前置通知,模拟执行权限检查...");
System.out.println("目标类是:"+joinpoint.getTarget());
System.out.println("被植入增强处理的目标方法为:"+joinpoint.getSignature().getName());
}
//后置通知
public void myAfterReturning(JoinPoint joinpoint){
System.out.println("后置通知,模拟记录日志...");
System.out.println("目标类是:"+joinpoint.getTarget());
System.out.println("被植入增强处理的目标方法为:"+joinpoint.getSignature().getName());
}
/*环绕通知
proceedingJoinPoint是JoinPoint子接口,表示可以执行目标方法
1.必须是Object类型的返回值
2.必须接受一个参数,类型为proceedingJoinPoint
3.必须throws Throwable
*/
public Object myAround(ProceedingJoinPoint proceedingJoinPoint)throws Throwable{
//开始
System.out.println("环绕开始:执行目标方法之前,模拟开启事务...");
//执行当前目标方法
Object obj=proceedingJoinPoint.proceed();
//结束
System.out.println("环绕结束:执行目标方法之后,模拟关闭事务...");
return obj;
}
//异常通知
public void myAfterThrowing(JoinPoint joinpoint,Throwable e){
System.out.println("异常通知:"+"出错了"+e.getMessage());
}
//最终通知
public void myAfter(){
System.out.println("最终通知:模拟方法结束后的释放资源");
}
}
(2)applicationContext.xml
<!--1.目标类-->
<bean id="userDao" class="com.itcast.jdk.UserDaoImpl"/>
<!--2.切面-->
<bean id="myAspect" class="com.itcast.aspectj.xml.MyAspect"/>
<!--3.aop编程-->
<aop:config>
<!--配置切面-->
<aop:aspect ref="myAspect">
<!--3.1配置切入点,通知最后增强哪些方法-->
<aop:pointcut expression="execution(* com.itcast.jdk.*.*(..))" id="myPointCut">
<!--3.2关联通知Advice和切入点pointcut-->
<!--3.2.1前置通知-->
<aop:before method="myBefore" pointcut-ref="myPointCut"/>
<!--3.2.2后置通知,在方法返回之后执行,就可以获得返回值
returning属性:用于设置后置通知的第二个参数的名称,类型是Object-->
<aop:after-returning method="myAfterReturning"
pointcut-ref="myPointCut" returning="returnVal"/>
<!--3.2.3环绕通知-->
<aop:around method="myAround" pointcut-ref="myPointCut"/>
<!--3.2.4抛出通知:用于处理程序发生异常-->
<!--如果程序没有异常,将不会执行增强-->
<!--throwing属性:用于设置通知第二个参数的名称,类型Throwable-->
<aop:after-throwing method="myAfterThrowing" pointcut-ref="myPointCut" throwing="e"/>
<!--3.2.5最终通知无论程序发生任何事情,都将执行-->
<aop:after method="myAfter" pointcut-ref="myPointCut">
</aop:aspect>
</aop:config>
(3)测试类TestXmlAspectj
//测试类
public class TestXmlAspectj{
public static void main(String[]args){
String xmlpath="com/itcast/aspectj/xml/applicationContext.xml";
ApplicationContext applicationContext=new ClassPathXmlApplicationContext(xmlpath);
//1.从spring容器获得内容
UserDao userDao=(UserDao)applicationContext.getBean("userDao");
//2.执行方法
userDao.addUser();
}
}
5.2基于注解的声明式AspectJ
@Aspect | 用于定义一个切面 |
@Pointcut | 实际上,这个方法签名就是一个返回值为void,且方法体为空的普通的方法.用于定义切入点表达式.在使用时还需定义一个包含名字和任意参数的方法签名来表示切入点名称. |
@Before | 用于定义前置通知,相当于BeforeAdvice.在使用时,通常需要指定一个value属性值,该属性值用于指定一个切入点表达式(可以是已有的切入点,也可以直接定义切入点表达式) |
@AfterReturning | 用于定义后置通知,相当于AfterReturningAdvice,在使用时可以指定pointcut/value和returning属性,其中,pointcut/value这两个属性的作用一样,都用于指定切入点表达式.Returning属性用于表示Advice方法中可定义与此同名的形参,该形参可用于访问目标方法的返回值 |
@Around | 用于定义环绕通知,相当于MethodInterceptor,在使用时需要指定一个value属性,该属性用于指定被植入的切入点 |
@AfterThrowing | 用于定义异常通知来处理程序中未处理的异常,相当于ThrowAdvice.在使用时可指定pointcut/value和throwing属性.其中pointcut/value用于指定切入点表达式,而throwing属性值用于指定一个形参名来表示Advice方法中可定义与此同名的形参,该形参可用于访问目标方法跑出的异常 |
@After | 用于定义最终final通知,不管是否异常,该通知都会执行.使用时需要指定一个value属性,该属性用于指定该通知被植入的切入点 |
@DeclareParents | 用于定义引介通知,相当于IntroductionInterceptor |