Spring AOP的基础就是动态代理模式
1、接口,JDK动态代理要求接口
public interface IHello {
public String sayHello(String name);
}
2、实现类
public class HelloImpl implements IHello{
@Override
public String sayHello(String name) {
return "Hello "+name+"!";
}
}
3、方法回调类
public class MethodInvocation implements InvocationHandler {
private Object target;//被代理对象
public MethodInvocation(Object target){
this.target=target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("======================前置处理======================");
System.out.println("调用的方法为:"+method);
if(args!=null && args.length>0){
for(Object tmp:args)
System.out.println("调用方法的参数:"+tmp);
}else{
System.out.println("没有参数");
}
//调用目标对象的方法
Object res=method.invoke(target,args);
System.out.println("======================后置处理======================");
System.out.println("方法的执行结果为:"+res);
return "修改返回结果:"+res;
}
}
4、编程调用
//被代理对象
IHello hello=new HelloImpl();
//代理对象,没有具体的实现类
IHello proxy= (IHello) Proxy.newProxyInstance(IHello.class.getClassLoader(),new Class[]{IHello.class},new MethodInvocation(hello));
//调用代理对象的方法,从而达到执行被代理对象方法的目的
String res= proxy.sayHello("巡航");
System.out.println("Main:"+res);
SpringAOP编程基础步骤:
1、添加依赖:spring-context-support、spring-aop、spring-aspects
2、前置处理
public class BeforMethod implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("======================前置处理======================");
System.out.println("调用的方法为:"+method);
if(args!=null && args.length>0){
for(Object tmp:args)
System.out.println("调用方法的参数:"+tmp);
}else{
System.out.println("没有参数");
}
}
}
3、配置 beans.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="hello" class="com.yan.HelloImpl"/>
<!--前置处理对象 -->
<bean id="beforeHello" class="com.yan1.BeforMethod"/>
<!-- 代理对象 -->
<bean id="proxyHello" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="hello"/>
<property name="interfaces">
<value>com.yan.IHello</value>
</property>
<property name="interceptorNames">
<value>beforeHello</value>
</property>
</bean>
</beans>
4、编程调用
public class Test {
public static void main(String[] args) {
ApplicationContext ac=new ClassPathXmlApplicationContext("yan1/beans.xml");
IHello hello=ac.getBean("proxyHello", IHello.class);
String res= hello.sayHello("张三");
System.out.println("Main:"+res);
}
}
SpringAOP基础概念
连接点、通知(增强)、切入点、切面、代理
Spring的通知类型
Spring通知类型按切面功能调用的不同时刻,可以分为提供了5种Advice类型
1、前置通知Before advice:在某连接点之前执行的通知,但这个通知不能阻止连接点之前的执行流程(除非它抛出一个异常)。
2、后置通知After returning advice:在某连接点正常完成后执行的通知
3、异常通知After throwing advice:在方法抛出异常退出时执行的通知
4、最终通知After (finally) advice:当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)
5、环绕通知Around Advice:包围一个连接点的通知,如方法调用。这是最强大的一种通知类型。环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它自己的返回值或抛出异常来结束执行另有引介增强introductioninterceptor表示在目标类中添加一些新的方法和属性,通过拦截定义一个接口,让目标代理实现这个接口。Spring为该接口提供了DelegatingIntroductionInterceptor实现类,一般情况下,通过扩展该实现类定义自己的引介增强类
通知类型的选择
1、环绕通知是最常用的通知类型
2、推荐使用尽可能简单的通知类型来实现需要的功能。如果你只是需要一个方法的返回值来更新缓存,最好使用后置通知而不是环绕通知
3、用最合适的通知类型可以使得编程模型变得简单,并且能够避免很多潜在的错误
通知实现方法:
1、实现接口的方法 public class BeforMethod implements MethodBeforeAdvice
2、定义通知类+aop命名空间
3、定义切面类进行注解开发
4、JavaConfig配置类定义方式
前置通知:
开发方法1:实现接口
public class BeforMethod implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("======================前置处理======================");
System.out.println("被代理的对象:"+target);
System.out.println("调用的方法为:"+method);
if(args!=null && args.length>0){
for(Object tmp:args)
System.out.println("调用方法的参数:"+tmp);
}else{
System.out.println("没有参数");
}
}
}
<!-- 目标对象 -->
<bean id="hello" class="com.yan.HelloImpl"/>
<!--前置处理对象 -->
<bean id="beforeHello" class="com.yan1.BeforMethod"/>
<!-- 代理对象 -->
<bean id="proxyHello" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="hello"/>
<property name="interfaces">
<value>com.yan.IHello</value>
</property>
<property name="interceptorNames">
<value>beforeHello</value>
</property>
</bean>
问题1:是否可以修改传入参数
可以修改传入参数
if(args!=null && args.length>0){
args[0]="修改传入参数";
}
问题2:是否可以决定程序继续向后执行
不能决定程序是否继续执行,前置处理执行完毕,则一定之后后续逻辑。除非人为抛出异常终止执行
throw new RuntimeException(“出问题了!”);
开发方法2:定义通知类+aop命名空间
2.1、还需要依赖接口
<bean id="hello" class="com.yan.HelloImpl"/>
<bean id="bef02" class="com.bef02.Bef02Advice"/>
<aop:config>
<!--定义切入点,切入点就是连接点的对象化表示
aop:pointcut :切入点
expression :切入点表达式
execution(* *(..)) :代表所有方法都作为切入点加入额外功能
-->
<aop:pointcut id="bef02Aspect" expression="execution(* com.yan.Hello*.*(..))"/>
<!-- 定义通知者,通知者用于将通知代码织入到目标程序 -->
<!--组装:目的把切入点 与 额外功能 进行整合
表达的含义:所有的方法 都加入 before的额外功能 -->
<aop:advisor advice-ref="bef02" pointcut-ref="bef02Aspect"/>
</aop:config>
2.2、不使用接口
//切面类,不需要实现任何接口,可以自定义方法
public class Bef03Aspect {
public void pp(){
System.out.println("前置处理....");
}
}
<bean id="hello" class="com.yan.HelloImpl"/>
<bean id="aspect1" class="com.bef03.Bef03Aspect"/>
<aop:config>
<aop:pointcut id="pointcut1" expression="execution(* com.yan.*.*(..))"/>
<aop:aspect ref="aspect1">
<aop:before method="pp" pointcut-ref="pointcut1"/>
</aop:aspect>
</aop:config>
ApplicationContext ac=new ClassPathXmlApplicationContext("bef03/beans.xml");
IHello hello=ac.getBean("hello",IHello.class);
String res=hello.sayHello("zhangsan");
System.out.println("Main:"+res);
如何获取连接点的相关数据,例如调用的方法、方法的参数等
1、所定义的方法范围限定词可以使用private,表示是通过反射进行调用
2、方法的返回值一般是void类型,实际上使用其他类型也可以,但是无法接收返回值
3、方法可以使用一个参数JoinPoint连接点对象,通过这个参数可以获取连接点相关信息
public class Bef03Aspect {
public void pp(JoinPoint joinPoint){
//获取所调用的方法签名
String methodName=joinPoint.getSignature().getName();
System.out.println("所调用的方法为:"+methodName);
//获取调用方法时的传入参数
Object[] args = joinPoint.getArgs();
if(args!=null && args.length>0){
for(int i=0;i<args.length;i++)
System.out.println("第"+i+"个参数为:"+args[i]);
}
//修改传入参数无效,除非是原地修改。例如将参数类型定义为StringBuilder或者StringBuffer
if(args!=null && args.length>0){
args[0]="修改传入参数"; //因为String用于字符串常量,针对String的任何操作都会引发对象的新建
}
Object target = joinPoint.getTarget();
System.out.println("目标对象为:"+target);
System.out.println("前置处理....");
}
}
<bean id="hello" class="com.yan.HelloImpl"/>
<bean id="aspect1" class="com.bef03.Bef03Aspect"/>
<aop:config>
<aop:pointcut id="pointcut1" expression="execution(* com.yan.*.*(..))"/>
<aop:aspect ref="aspect1">
<aop:before method="pp" pointcut-ref="pointcut1"/>
</aop:aspect>
</aop:config>
Spring框架中的AOP拦截技术是POJO的方法层面的拦截【拦截的颗粒度较粗】。其底层实现原理是动态代理技术。对于面向接口的方法拦截,依赖于jdk的动态代理技术,即java.lang.reflect.Proxy#newProxyInstance,将对被代理的目标对象的调用,委托到代理对象,触发拦截通知;而当被拦截的方法, 不是在接口中定义时,使用的是cglib,对字节码进行动态增强,生成被代理类的子对象,以实现代理
spring实现aop,动态代理技术的两种实现是jdk动态代理、cglib代理,根据被通知的方法是否为接口方法,来选择使用哪种代理生成策略
jdk动态代理,原理是实现接口的实例,拦截定义于接口中的目标方法,性能更优,是spring生成代理的优先选择
cglib代理,原理是使用cglib库中的字节码动态生成技术,生成被代理类的子类实例,可以拦截代理类中的任一public方法的调用,无论目标方法是否定义于接口中,更通用,但性能相对jdk代理差一些;
3、定义切面类进行注解开发
@Component //通过自动组件扫描,将当前类声明为受管bean
@Aspect //用于声明当前类是一个切面类
public class BefAspect {
before用于表示方法需要前置执行,其中参数为切入点表达式。一般来说切点点表达式【AspectJ】不会这么具体,常用*通配符
@Before("execution(java.lang.String com.yan.HelloImpl.sayHello(java.lang.String))")
public void pp(JoinPoint joinPoint){
//获取所调用的方法签名
String methodName=joinPoint.getSignature().getName();
System.out.println("所调用的方法为:"+methodName);
//获取调用方法时的传入参数
Object[] args = joinPoint.getArgs();
if(args!=null && args.length>0){
for(int i=0;i<args.length;i++)
System.out.println("第"+i+"个参数为:"+args[i]);
}
//修改传入参数无效,除非是原地修改。例如将参数类型定义为StringBuilder或者StringBuffer
if(args!=null && args.length>0){
args[0]="修改传入参数"; //因为String用于字符串常量,针对String的任何操作都会引发对象的新建
}
Object target = joinPoint.getTarget();
System.out.println("目标对象为:"+target);
System.out.println("前置处理....");
}
由于使用注解开发,所以直接使用自动组件扫描,不用一个一个的配置受管bean
<?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"
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
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com"/>
<!--声明自动代理 -->
<aop:aspectj-autoproxy/>
</beans>
4、JavaConfig配置+注解开发
@Configuration//用于声明当前类是一个配置类
@EnableAspectJAutoProxy //自动生成代理
@ComponentScan("com")//自动组件扫描
public class JavaConfig {
}
@Component
@Aspect //用于声明当前类是一个切面类
public class BefAspect {
@Before("execution(* com.yan.*.sayHello(..))")
public void pp(JoinPoint joinPoint){
//获取所调用的方法签名
String methodName=joinPoint.getSignature().getName();
System.out.println("所调用的方法为:"+methodName);
//获取调用方法时的传入参数
Object[] args = joinPoint.getArgs();
if(args!=null && args.length>0){
for(int i=0;i<args.length;i++)
System.out.println("第"+i+"个参数为:"+args[i]);
}
//修改传入参数无效,除非是原地修改。例如将参数类型定义为StringBuilder或者StringBuffer
if(args!=null && args.length>0){
args[0]="修改传入参数"; //因为String用于字符串常量,针对String的任何操作都会引发对象的新建
}
Object target = joinPoint.getTarget();
System.out.println("目标对象为:"+target);
System.out.println("前置处理....");
}
}
没有xml配置文件
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(JavaConfig.class);
IHello hello=ac.getBean("hello",IHello.class);
String res=hello.sayHello("zhangsan");
System.out.println("Main:"+res);
切入点表达式的使用
//execution(* com.yan.biz.Hello*.(…))切入点表达式,是AspectJ提供的一种用于表述切入点的语法
//表示返回任意类型,也可以是java.lang.String表示返回String类型
//com.yan.biz.Hello表示是com.yan.biz包中所有以Hello开头的类。如果直接表示任意类
//.表示类中的任意方法,也可以使用.say表示以say开头的任意方法
//(…)表示任意个数的任意类型参数,也可以使用(java.lang.String)表示有一个String类型的参数
可以通过JoinPoint参数获取连接点的相关信息:例如方法名称、参数等。可以在通知方法中声明一个类型为
JoinPoint 的参数. 然后就能访问链接细节. 如方法名称和参数值
环绕通知编程:
1、对于环绕通知来说, 连接点的参数类型必须是ProceedingJoinPoint,它是JoinPoint的子接口, 允许控制何时执
行, 是否执行连接点.
2、在环绕通知中需要明确调用ProceedingJoinPoint的proceed()方法来执行被代理的方法. 如果忘记这样做就会导致通知被执行了, 但目标方法没有被执行.
注意: 环绕通知的方法需要返回目标方法执行之后的结果, 即调用joinPoint.proceed()的返回值, 否则会出现空指针异常
@Component
@Aspect
public class AroundAspect {
@Around("execution(* com..*.*(..))")
public Object pp(ProceedingJoinPoint jp) throws Throwable{
Object res=null;
try{
System.out.println("环绕前置开始....");
System.out.println("调用的方法名称"+jp.getSignature().getName());
Object[] args = jp.getArgs();
if(args!=null && args.length>0)
System.out.println("调用方法的参数为:"+ Arrays.toString(args));
System.out.println("调用的目标对象为:"+jp.getTarget());
System.out.println("环绕前置结束....");
//修改传入参数
if(args!=null && args.length>0)
args[0]="修改传入参数";
//继续向后执行
// res=jp.proceed(); 用于不修改传入参数的情况下
res=jp.proceed(args); //用于修改传入参数的情况下
System.out.println("环绕后置开始....");
System.out.println("执行结果为:"+res);
System.out.println("环绕后置结束....");
//可以修改返回数据
res="可以修改业务处理的返回结果";
}catch (Throwable throwable){
System.out.println("环绕异常开始....");
System.out.println("执行异常为:"+throwable);
System.out.println("环绕异常结束.....");
}finally{
System.out.println("环绕最终开始....");
System.out.println("环绕最终结束");
}
return res;
}
}
SpringAOP特点
1、实现了分散关注,将通用需求功能从不相关类之中分离出来;同时,能够使得很多类共享一个行为,一旦行为发生变化,不必修改很多类,只要修改这个行为就可以
2、允许开发者使用声明式企业服务,比如事务服务、安全性服务
3、开发者可以开发满足业务需求的自定义方面
4、开发Spring AOP Advice 很方便,可以借助代理类快速搭建Spring AOP 应用
AOP与IoC的联系与区别 [简单面试]
相同点:
都是寻求调用者与被调用者的解耦
不同点:
1、IoC关注的是对象的创建、维护职责与对象的使用权(该使用权与调用者的功能紧密相关)解耦
2、AOP关注的是功能本身的解耦
SpringAOP原理 【重点记忆】
Spring AOP是由BeanPostProcessor后置处理器开始的,这个后置处理器是一个监听器,可以监听容器触发的Bean生命周期事件,向容器注册后置处理器以后,容器中管理的Bean就具备了接收IoC容器回调事件的能力。BeanPostProcessor的调用发生在Spring IoC容器完成Bean实例对象的创建和属性的依赖注入之后,为Bean对象添加后置处理器的入口是initializeBean方法。Spring中JDK动态代理生通过JdkDynamicAopProxy调用Proxy的newInstance方法来生成代理类,JdkDynamicAopProxy也实现了InvocationHandler接口,invoke方法的具体逻辑是先获取应用到此方法上的拦截器链,如果有拦截器则创建MethodInvocation并调用其proceed方法,否则直接反射调用目标方法。因此Spring AOP对目标对象的增强是通过拦截器实现的。
AOP总结
1、软件系统可看作由一组关注点组成。其中,直接的业务关注点,是直切关注点。而为直切关注点提供服务的,就是横切关注点
2、有两种方法可以提供横切关注点,一种是传统的OOP方法,提供一个与直切关注点的实现一样的类来提供服务。另一种是最新的AOP方法,提供一个Aspect方面(Spring AOP中叫advisor)来提供服务
3、使用动态代理模拟实现AOP,例如JBoss AOP框架和Spring AOP框架都使用这种方式
4、Spring AOP的advice代码只能够在连接点方法之前、之后调用,或者在异常被抛出之后调用
5、Spring AOP框架提供横切关注点服务的方式。编写一个方面,这个方面也是一个一般的pojo类,但是它需要提供一个接口,然后,使用Advisor(就是方面,是方面+切入点)进行配置,配置接口和切入点的模式
6、利用Spring非常强大的IoC容器和AOP功能,能实现非常灵活的应用,让Spring容器管理业务对象的生命周期,利用AOP增强功能,却不影响业务接口,从而避免更改客户端代码。
为了实现目标,必须始终牢记:面向接口编程。而默认的AOP代理也是通过Java的代理接口实现的。虽然也可以用CGLIB实现对普通类的代理,但是,业务对象只要没有接口,就会变得难以扩展、维护和测试