AOP概述
AOP定义
AOP:Aspect Oriented Programming(面向切面编程),是通过预编译和运行期动态代理来实现程序功能的统一维护的技术
不同的业务块有时会具有相同的操作,如图:
将这样相同的操作提取出来就是切面,aop则是面向这些多个业务块横向切取的公共片段编程,在维护期间,仅需要对切面进行修改即可,降低了耦合度,可维护性大大增强
将切面提取之后,原对象和切面就被分隔开:
此时aspect和pointcut互不相关,交由代理将其联系在一起,调用时直接通过代理获取目标对象即可
相关概念
- Advice,通知:需要单独封装的功能,定义在类的方法中;通知定义Pointcut中的具体需要执行的操作
- 前置通知:在目标方法执行之前执行
- 环绕通知:在目标方法执行之前和之后都可以执行
- 后置通知:在目标方法执行之后执行
- 异常通知:在目标方法抛出异常时执行
- 最终通知:在目标方法执行之后执行,不同于后置通知的是,后置通知只有程序正常时执行,抛出异常时后置通知不会执行;而最终通知不论是否异常都会执行
- JoinPoint,连接点:可以使用通知的地方,如事务控制中,使用到事务控制的方法;自身可嵌套其他的JoinPoint
- Pointcut,切入点:定义使用通知的连接点的集合,与连接点是一对多的关系
- Aspect,切面:通知和切入点的组合
- Weaving,织入:把切面应用到应用程序中的过程
- Target,目标:应用切面的对象
- Introduction,引入:向现有的类添加新方法或新属性
应用场景
- 日志记录
- 事务管理
- 安全控制
- 异常处理
- 性能统计
SpringAOP实现方式(编码方式、xml方式、命名空间方式)
- 前置通知:实现MethodBeforeAdvice接口
- 后置通知:实现AfterReturningAdvice接口
- 环绕通知:实现MethodInterceptor接口;环绕通知返回结果为方法的返回值,简单的切面可以只需要用环绕通知就实现了前置、后置、异常通知的功能
- 异常通知:实现ThrowsAdvice接口;异常通知并没有任何方法,如果使用该接口则必须实现这样格式的方法void afterThrowing([method,args,target],throwable);其中出了异常对象,其他的都可以去掉
代码演示
目标接口
public interface Service {
public void add();
public void update() throws Exception;
}
目标实现类
public class MyBeforeAdvice implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("MyBeforeAdvise before");
}
}
前置通知
public class MyBeforeAdvice implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("before");
}
}
后置通知
public class MyAfterAdvice implements AfterReturningAdvice {
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("after");
}
}
环绕通知
public class MyRoundAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("开始");
Object result = invocation.proceed();//执行目标对象的方法,并获取方法返回值
System.out.println("结束");
return result;
}
}
异常通知
public class MyThrowsAdvice implements ThrowsAdvice {
public void afterThrowing(Method method,Object[] args,Object target, Exception ex){
System.out.println("Exception");
}
}
编码方式
使用ProxyFacoty类的方法setTarget()和addAdvice(),代理使用getProxy获取
代理工厂类
public class MyProxyFactory {
public static <T>T getProxy(Class<T> c) throws Exception {
T t = c.newInstance();
MyBeforeAdvice myBeforeAdvise = new MyBeforeAdvice();
MyAfterAdvice myAfterAdvice = new MyAfterAdvice();
MyRoundAdvice myRoundAdvice = new MyRoundAdvice();
MyThrowsAdvice myThrowsAdvice = new MyThrowsAdvice();
//创建代理工厂
ProxyFactory proxyFactory = new ProxyFactory();
//设置目标与添加切面,将目标与切面联系在一起
// proxyFactory.setInterfaces(Service.class);该语句为设置实现的接口,当代理接口时使用setInterfaces方法可以指定代理的接口,spring将使用jdk的代理;代理类或没有setInterfaces时spring将使用cglib的代理
proxyFactory.setTarget(t);
proxyFactory.addAdvice(myBeforeAdvise);
proxyFactory.addAdvice(myAfterAdvice);
proxyFactory.addAdvice(myRoundAdvice);
proxyFactory.addAdvice(myThrowsAdvice);
//获取代理对象
return (T) proxyFactory.getProxy();
}
}
测试
public class TestBycode {
@Test
public void test1() throws Exception {
Service proxy = MyProxyFactory.getProxy(ServiceImpl.class);
proxy.add();
proxy.update();
}
}
结果
before
开始
add
结束
after
15:07:45.857 [main] DEBUG o.s.a.f.a.ThrowsAdviceInterceptor - Found exception handler method on throws advice: public void com.aop.bycode.MyThrowsAdvice.afterThrowing(java.lang.reflect.Method,java.lang.Object[],java.lang.Object,java.lang.Exception)
before
开始
update
Exception
java.lang.Exception
at com.aop.bycode.ServiceImpl.update(ServiceImpl.java:20)
at com.aop.bycode.ServiceImpl$$FastClassBySpringCGLIB$$9c5bb4a4.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:779)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
at org.springframework.aop.framework.adapter.ThrowsAdviceInterceptor.invoke(ThrowsAdviceInterceptor.java:113)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
at com.aop.bycode.MyRoundAdvice.invoke(MyRoundAdvice.java:18)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
at org.springframework.aop.framework.adapter.AfterReturningAdviceInterceptor.invoke(AfterReturningAdviceInterceptor.java:57)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
at org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor.invoke(MethodBeforeAdviceInterceptor.java:58)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:692)
at com.aop.bycode.ServiceImpl$$EnhancerBySpringCGLIB$$3796a5e3.update(<generated>)
at aop.bycode.TestBycode.test1(TestBycode.java:41)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:221)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)
Process finished with exit code -1
代理类时,spring将使用cglib的代理
代理的类实现了接口时,需要指定代理的接口类型,即使用proxyFactory.setIntegerfaces方法,否则还是使用cglib代理的实现类,指定接口之spring将使用jdk代理接口
xml方式
在spring容器中配置需要代理的目标对象的bean以及通知的bean,然后配置ProxyFactoryBean,指定属性target和interceptorNames,配置好的ProxyFactoryBean就是代理的bean直接注入即可
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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="serviceImpl" class="com.aop.ServiceImpl"/>
<bean id="myAfterAdvice" class="com.aop.MyAfterAdvice"/>
<bean id="myBeforeAdvice" class="com.aop.MyBeforeAdvice"/>
<bean id="myRoundAdvice" class="com.aop.MyRoundAdvice"/>
<bean id="myThrowsAdvice" class="com.aop.MyThrowsAdvice"/>
<!--在配置文件中配置一个ProxyFactoryBean,可以得到一个对应代理的Bean-->
<bean id="serviceProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<!--设置代理的目标-->
<property name="target" ref="serviceImpl"/>
<property name="interceptorNames">
<array>
<value>myAfterAdvice</value>
<value>myBeforeAdvice</value>
<value>myRoundAdvice</value>
<value>myThrowsAdvice</value>
</array>
</property>
</bean>
</beans>
使用xml方式不需要指定接口类型,如果代理的类实现了接口,则会使用jdk代理,否则会使用cglib动态代理
命名空间方式(最简洁方便)
execution表达式(切点函数):定义切入点
权限修饰符返回类型 包名.类名.方法名.(参数)
其中,出参数外,其他部分用*表示所有的,参数用…表示所有的
常用的表达式:
(* package.*.*(..))
定义切点为package包中的所有类的任何参数的方法public* package.*.*(..)
定义切点为package包中的所有public类的任何参数的方法
通过spring配置文件,引入aop命名空间,通过aop-config标签来配置目标和切面之间的联系
配置文件
<?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="serviceImpl" class="com.aop.target.ServiceImpl"/>
<bean id="myAfterAdvice" class="com.aop.aspect.MyAfterAdvice"/>
<bean id="myBeforeAdvice" class="com.aop.aspect.MyBeforeAdvice"/>
<bean id="myRoundAdvice" class="com.aop.aspect.MyRoundAdvice"/>
<bean id="myThrowsAdvice" class="com.aop.aspect.MyThrowsAdvice"/>
<aop:config>
<aop:pointcut id="myPointcut" expression="execution(* com.aop.target.*.*(..))"/>
<aop:advisor advice-ref="myThrowsAdvice" pointcut-ref="myPointcut"/>
<aop:advisor advice-ref="myBeforeAdvice" pointcut-ref="myPointcut"/>
<aop:advisor advice-ref="myAfterAdvice" pointcut-ref="myPointcut"/>
<aop:advisor advice-ref="myRoundAdvice" pointcut-ref="myPointcut"/>
</aop:config>
<aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>
测试
public class TestByNamespace {
@Test
public void test1() throws Exception {
ApplicationContext context = new ClassPathXmlApplicationContext("aopNamespace.xml");
ServiceImpl proxy = context.getBean("serviceImpl",ServiceImpl.class);
proxy.add();
}
}
使用命名空间的注意事项
- 代理实现类时的误操作
当代理的是实现类时,spring默认使用的是jdk实现动态代理,在通过getbean获取实例时,如果使用getbean(name,classType)方式的话,会报错,因为从spring容器中得到的实例是jdk的Proxy类型,此时则需要使用getBean(name)的方式,然后将获取到的对象强转;或者使用
<aop:aspectj-autoproxy proxy-target-class="true"/>
将代理主动设置为cglib的形式,由于cglib代理是通过创建子类的方式,所以传入的目标类的class和获取到的类型是符合的
- 通知执行的顺序问题
使用命名空间时,定义通知的标签会重新排列通知执行的顺序,当前置通知、后置通知、异常通知生民在环绕通知之前时,会按照原顺序执行;当任一个通知定义在环绕通知之后时,该通知会和环绕通知的前置环绕或后置环绕顺序调换