代理模式:给某一个对象提供一个代理对象,并由代理对象控制对源对象的引用。代理 就是一个人或一个机构代表另一个人或者一个机构采取行动。某些情况下,客户不想或者不 能够直接引用一个对象,代理对象可以在客户和目标对象直接起到中介的作用。客户端分辨 不出代理主题对象与真实主题对象。代理模式可以并不知道真正的被代理对象,而仅仅持有 一个被代理对象的接口,这时候代理对象不能够创建被代理对象,被代理对象必须有系统的 其他角色代为创建并传入。 为什么要使用代理模式呢? 第一,它有间接的特点,可以起到中介隔离作用。就好比在租房的时候,房东可能不在 本地,而短期内又不能赶回来,此时中介的出场,就作为房东的代理实现和我们签订承租合 同。而我们和房东之间就没有耦合了。 第二,它有增强的功能。还以租房为例,我们首先考虑的是找一个靠谱的中介,由中介 给我们提供房源信息,并且告诉我们房屋的细节,合同的细节等等。当然我们也可以自己去 找一些个人出租的房屋,但是在这之中,我们要了解租赁合同,房屋验收,租金监管等情 况,这无疑对我们是繁重的工作。而有了中介作为我们的代理中间人,他把了解到的信息告 诉我们,我们一样可以租到房子,而不必做那些繁重的工作。
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通 过预编译方 式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中 的一个 热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对 业务逻辑 的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性, 同时提高 了开发的效率。
在上面的概念中描述出aop的实现原理是基于动态代理技术实现的。下面是针对动态代 理的一些介绍:
特点: 字节码随用随创建,随用随加载
分类: 基于接口的动态代理,基于子类的动态代理
作用: 不修改源码的基础上对方法增强 基于接口的动态代理:
提供者是:JDK官方
使用要求:被代理类最少实现一个接口。
涉及的类:Proxy
创建代理对象的方法:newProxyInstance
方法的参数: ClassLoader:类加载器。用于加载代理对象的字节码的。和被代理对象使 用相同的类加载器。固定写法。
Class[]:字节码数组。用于给代理对象提供方法。和被代理对象具有相同的 方法。 被代理类是一个普通类:
被代理类对 象.getClass().getInterfaces();
被代理类是一个接口:new Class[]{被代理了.class}
它也是固定写法
InvocationHanlder:要增强的方法。此处是一个接口,我们需要提供它的 实现类。 通常写的是匿名内部类。增强的代码谁用谁写。 基于子类的动态代理
提供者是:第三方cglib包,在使用时需要先导包(maven工程导入坐标即可)
使用要求:被代理类不能是最终类,不能被final修饰
涉及的类:Enhancer
创建代理对象的方法:create
方法的参数: Class:字节码。被代理对象的字节码。可以创建被代理对象的子类,还可以 获取被代理对象的类加载器。
Callback:增强的代码。谁用谁写。通常都是写一个接口的实现类或者匿名 内部类。
Callback中没有任何方法,所以我们一般使用它的子接口: MethodInterceptor
Joinpoint(连接点):
所谓连接点是指那些被拦截到的点。在spring中,指的是方法,因为spring只支持方 法类型的连接点。
Pointcut(切入点):
所谓切入点是指我们要对哪些Joinpoint进行拦截的定义。
Advice(通知/增强):
所谓通知是指拦截到Joinpoint之后所要做的事情就是通知。 通知的类型:前置通知,后置通知,异常通知,最终通知,环绕通知。
Introduction(引介):
引介是一种特殊的通知在不修改类代码的前提下, 可以在运行期为类动态地添加一 些方法或Field。
Target(目标对象): 代理的目标对象。
Weaving(织入):
是指把增强应用到目标对象来创建新的代理对象的过程。 spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。
Proxy(代理): 一个类被AOP织入增强后,就产生一个结果代理类。
Aspect(切面):
是切入点和通知(引介)的结合。
a.Spring的aop是基于ioc的。所以需要有spring的ioc基础。(本篇内容不对ioc进行讲 解)
b.本章节我们只是对aop的使用做基本功能展示,目的是为了以此讲解aop中的注解和执行原 理分析。
需求: 实现在执行service方法时输出执行日志。(除了业务层外,表现层和持久层也可 以实现)
实体类:(在本案例中没有实际作用) /** * @author 黑马程序员 * @Company http://www.itheima.com */ public class User implements Serializable { private String id; private String username; private String password; private String email; private Date birthday; private String gender; private String mobile; private String nickname; } 业务层接口: /** * @author 黑马程序员 * @Company http://www.itheima.com */ public interface UserService { /** * 保存用户 * @param user */ void save(User user); } 业务层实现类: /** * 用户的业务层实现类 * @author 黑马程序员 * @Company http://www.itheima.com */ @Service("userService") public class UserServiceImpl implements UserService{ @Override public void save(User user) { System.out.println("保存用:"+user); } } 日志工具类: /** * 记录日志的工具类 * @author 黑马程序员 * @Company http://www.itheima.com */ @Component @Aspect public class LogUtil { /** * 通用切入点表达式 */ @Pointcut("execution(* com.itheima.service.impl.*.*(..))") private void pt1(){} /** * 前置通知 */ @Before("pt1()") public void beforeLog(){ System.out.println("执行切入点方法前记录日志"); } /** * 后置通知 */ @AfterReturning("pt1()") public void afterReturningLog(){ System.out.println("正常执行切入点方法后记录日志"); } /** * 异常通知 */ @AfterThrowing("pt1()") public void afterThrowingLog(){ System.out.println("执行切入点方法产生异常后记录日志"); } /** * 最终通知 */ @After("pt1()") public void afterLog(){ System.out.println("无论切入点方法执行是否有异常都记录日志"); } /** * 环绕通知 */ @Around("pt1()") public Object arountPrintLog(ProceedingJoinPoint pjp){ //1.定义返回值 Object rtValue = null; try{ //前置通知 System.out.println("执行切入点方法前记录日志"); //2.获取方法执行所需的参数 Object[] args = pjp.getArgs(); //3.执行切入点方法 rtValue = pjp.proceed(args); //后置通知 System.out.println("正常执行切入点方法后记录日志"); }catch (Throwable t){ //异常通知 System.out.println("执行切入点方法产生异常后记录日志"); }finally { //最终通知 System.out.println("无论切入点方法执行是否有异常都记录日志"); } return rtValue; } } 配置类: @Configuration @ComponentScan("com.itheima") @EnableAspectJAutoProxy public class SpringConfiguration { } 测试类: /** * spring的aop环境准备 * @author 黑马程序员 * @Company http://www.itheima.com */ public class SpringAOPTest { public static void main(String[] args) { //1.获取容器 AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class); //2.获取bean对象 UserService userService = ac.getBean("userService",UserService.class); //3.准备数据 User user = new User(); user.setId("1"); user.setUsername("test"); user.setNickname("泰斯特"); //4.执行方法 userService.save(user); } }
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Import(AspectJAutoProxyRegistrar.class) public @interface EnableAspectJAutoProxy { /** * Indicate whether subclass‐based (CGLIB) proxies are to be created as opposed * to standard Java interface‐based proxies. The default is {@code false}. */ boolean proxyTargetClass() default false; /** * Indicate that the proxy should be exposed by the AOP framework as a {@code ThreadLocal} * for retrieval via the {@link org.springframework.aop.framework.AopContext} class. * Off by default, i.e. no guarantees that {@code AopContext} access will work. * @since 4.3.1 */ boolean exposeProxy() default false; }
作用:
表示开启spring对注解aop的支持。它有两个属性,分别是指定采用的代理方式和 是否暴露代理对象,通过AopContext可以进行访问。从定义可以看得出,它引入 AspectJAutoProxyRegister.class对象,该对象是基于注解@EnableAspectJAutoProxy 注册一个AnnotationAwareAspectJAutoProxyCreator,该对象通过调用 AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(regist ry);注册一个aop代理对象生成器。关于AnnotationAwareAspectJAutoProxyCreator请参 考第五章第二小节《AnnotationAwareAspectJAutoProxyCreator对象的分析》 属性:
proxyTargetClass: 指定是否采用cglib进行代理。默认值是false,表示使用jdk的代理。
exposeProxy: 指定是否暴露代理对象,通过AopContext可以进行访问。 使用场景: 当我们注解驱动开发时,在需要使用aop实现某些功能的情况下,都需要用到此注 解。
/** * @author 黑马程序员 * @Company http://www.itheima.com */ @Configuration @ComponentScan("com.itheima") @EnableAspectJAutoProxy public class SpringConfiguration { }
/** * Aspect declaration * * @author <a href="mailto:alex AT gnilux DOT com">Alexandre Vasseur</a> */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface Aspect { /** * Per clause expression, defaults to singleton aspect * <p/> * Valid values are "" (singleton), "perthis(...)", etc */ public String value() default ""; }
作用:
声明当前类是一个切面类。
属性: value: 默认我们的切面类应该为单例的。但是当切面类为一个多例类时,指定预 处理的切入点表达式。
用法是perthis(切入点表达式)。
它支持指定切入点表达式,或者是用@Pointcut修饰的方法名称(要求全 限定方法名)
使用场景:
此注解也是一个注解驱动开发aop的必备注解。
/** * 记录日志的工具类 * @author 黑马程序员 * @Company http://www.itheima.com */ @Component @Scope("prototype")//注意:通常情况下我们的切面类是不需要多例的。 @Aspect(value="execution(* com.itheima.service.impl.*.*(..))") public class LogUtil { /** * 用于配置当前方法是一个前置通知 */ @Before("execution(* com.itheima.service.impl.*.*(..))") public void printLog(){ System.out.println("执行打印日志的功能"); } }
/** * Pointcut declaration * * @author <a href="mailto:alex AT gnilux DOT com">Alexandre Vasseur</a> */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Pointcut { /** * The pointcut expression * We allow "" as default for abstract pointcut */ String value() default ""; /** * When compiling without debug info, or when interpreting pointcuts at runtime, * the names of any arguments used in the pointcut are not available. * Under these circumstances only, it is necessary to provide the arg names in * the annotation ‐ these MUST duplicate the names used in the annotated method. * Format is a simple comma‐separated list. */ String argNames() default ""; }
作用:
此注解是用于指定切入点表达式的。
属性: value: 用于指定切入点表达式。表达式的配置详解请参考第五章节第三小节《切 入点表达式的写法》
argNames: 用于指定切入点表达式的参数。参数可以是execution中的,也可以是 args中的。通常情况下不使用此属性也可以获得切入点方法参数。
使用场景: 在实际开发中,当我们的多个通知需要执行,同时增强的规则确定的情况下,就可 以把切入点表达式通用化。此注解就是代替xml中的标签,实现切入点表达 式的通用化。
@Component @Aspect public class LogUtil { /** * 通用切入点表达式 * 在value属性的中使用了&&符号,表示并且的关系。 * &&符号后面的args和execution一样,都是切入点表达式支持的关键字,表示匹配 参数。指定的内容 * 可以是全限定类名,或者是名称。当指定参数名称时,要求与方法中形参名称相 同。 * argNames属性,是定义参数的名称,该名称必须和args关键字中的名称一致。 */ @Pointcut(value = "execution(* com.itheima.service.impl.*.* (com.itheima.domain.User))&& args(user)",argNames = "user") private void pt1(User user){} }
/** * Before advice * * @author <a href="mailto:alex AT gnilux DOT com">Alexandre Vasseur</a> */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Before { /** * The pointcut expression where to bind the advice */ String value(); /** * When compiling without debug info, or when interpreting pointcuts at runtime, * the names of any arguments used in the advice declaration are not available. * Under these circumstances only, it is necessary to provide the arg names in * the annotation ‐ these MUST duplicate the names used in the annotated method. * Format is a simple comma‐separated list. */ String argNames() default ""; }
作用:
被此注解修饰的方法为前置通知。前置通知的执行时间点是在切入点方法执行之 前。 属性:
value:
用于指定切入点表达式。可以是表达式,也可以是表达式的引用。
argNames:
用于指定切入点表达式参数的名称。它要求和切入点表达式中的参数名称 一致。通常不指定也可以获取切入点方法的参数内容。
使用场景:
在实际开发中,我们需要对切入点方法执行之前进行增强, 此时就用到了前置通 知。在通知(增强的方法)中需要获取切入点方法中的参数进行处理时,就要配合切入点表达 式参数来使用。
/** * 前置通知 */ @Before(value = "pt1(user)",argNames = "user") public void beforeLog(User user){ System.out.println("执行切入点方法前记录日志"+user); }
/** * After returning advice * * @author <a href="mailto:alex AT gnilux DOT com">Alexandre Vasseur</a> */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface AfterReturning { /** * The pointcut expression where to bind the advice */ String value() default ""; /** * The pointcut expression where to bind the advice, overrides "value" when specified */ String pointcut() default ""; /** * The name of the argument in the advice signature to bind the returned value to */ String returning() default ""; /** * When compiling without debug info, or when interpreting pointcuts at runtime, * the names of any arguments used in the advice declaration are not available. * Under these circumstances only, it is necessary to provide the arg names in * the annotation ‐ these MUST duplicate the names used in the annotated method. * Format is a simple comma‐separated list. */ String argNames() default ""; }
作用:
用于配置后置通知。后置通知的执行是在切入点方法正常执行之后执行。 需要注意的是,由于基于注解的配置时,spring创建通知方法的拦截器链时,后置 通知在最终通知之后,所以会先执行@After注解修饰的方法。
属性:
value: 用于指定切入点表达式,可以是表达式,也可以是表达式的引用。 pointcut: 它的作用和value是一样的。
returning: 指定切入点方法返回值的变量名称。它必须和切入点方法返回值名称一 致。
argNames: 用于指定切入点表达式参数的名称。它要求和切入点表达式中的参数名称 一致。通常不指定也可以获取切入点方法的参数内容。
使用场景:
此注解是用于配置后置增强切入点方法的。被此注解修饰方法会在切入点方法正常 执行之后执行。在我们实际开发中,像提交事务,记录访问日志,统计方法执行效率等等都可 以利用后置通知实现。
切入点方法: @Override public User findById(String id) { System.out.println("切入点方法开始执行。。。"); User user = new User(); user.setId(id); user.setUsername("heima"); user.setNickname("黑马小王子"); return user; } 后置通知: /** * 后置通知 */ @AfterReturning(value = "execution(* com.itheima.service.impl.*.* (..))&&args(param)",returning = "user") public void afterReturningLog(String param,Object user){ System.out.println("正常执行切入点方法后记录日志,切入点方法的参数 是:"+param); System.out.println("正常执行切入点方法后记录日志,切入点方法的返回值 是:"+user); }
/** * After throwing advice * * @author <a href="mailto:alex AT gnilux DOT com">Alexandre Vasseur</a> */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface AfterThrowing { /** * The pointcut expression where to bind the advice */ String value() default ""; /** * The pointcut expression where to bind the advice, overrides "value" when specified */ String pointcut() default ""; /** * The name of the argument in the advice signature to bind the thrown exception to */ String throwing() default ""; /** * When compiling without debug info, or when interpreting pointcuts at runtime, * the names of any arguments used in the advice declaration are not available. * Under these circumstances only, it is necessary to provide the arg names in * the annotation ‐ these MUST duplicate the names used in the annotated method. * Format is a simple comma‐separated list. */ String argNames() default ""; }
作用:
用于配置异常通知。
属性:
value: 用于指定切入点表达式,可以是表达式,也可以是表达式的引用。
pointcut: 它的作用和value是一样的。
throwing: 指定切入点方法执行产生异常时的异常对象变量名称。它必须和异常变量 名称一致。
argNames: 用于指定切入点表达式参数的名称。它要求和切入点表达式中的参数名称 一致。通常不指定也可以获取切入点方法的参数内容。
使用场景:
用此注解修饰的方法执行时机是在切入点方法执行产生异常之后执行。
切入点方法: @Override public User findById(String id) { System.out.println("切入点方法开始执行。。。"); User user = new User(); user.setId(id); user.setUsername("heima"); user.setNickname("黑马小王子"); int i=1/0; return user; } 通知方法: /** * 异常通知 */ @AfterThrowing(value = "execution(* com.itheima.service.impl.*.* (..))&&args(param)",throwing = "e") public void afterThrowingLog(String param,Throwable e){ System.out.println("执行切入点方法产生异常后记录日志,切入点方法的参数 是:"+param); System.out.println("执行切入点方法产生异常后记录日志,切入点方法的异常 是:"+e); }
/** * After finally advice * * @author <a href="mailto:alex AT gnilux DOT com">Alexandre Vasseur</a> */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface After { /** * The pointcut expression where to bind the advice */ String value(); /** * When compiling without debug info, or when interpreting pointcuts at runtime, * the names of any arguments used in the advice declaration are not available. * Under these circumstances only, it is necessary to provide the arg names in * the annotation ‐ these MUST duplicate the names used in the annotated method. * Format is a simple comma‐separated list. */ String argNames() default ""; }
作用:
用于指定最终通知。
属性:
value: 用于指定切入点表达式,可以是表达式,也可以是表达式的引用。
argNames: 用于指定切入点表达式参数的名称。它要求和切入点表达式中的参数名称 一致。通常不指定也可以获取切入点方法的参数内容。
使用场景:
最终通知的执行时机,是在切入点方法执行完成之后执行,无论切入点方法执行是 否产生异常最终通知都会执行。所以被此注解修饰的方法,通常都是做一些清理操作。
/** * 最终通知 */ @After(value = "execution(* com.itheima.service.impl.*.*(..))") public void afterLog(){ System.out.println("无论切入点方法执行是否有异常都记录日志"); }
/** * Around advice * * @author <a href="mailto:alex AT gnilux DOT com">Alexandre Vasseur</a> */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Around { /** * The pointcut expression where to bind the advice */ String value(); /** * When compiling without debug info, or when interpreting pointcuts at runtime, * the names of any arguments used in the advice declaration are not available. * Under these circumstances only, it is necessary to provide the arg names in * the annotation ‐ these MUST duplicate the names used in the annotated method. * Format is a simple comma‐separated list. */ String argNames() default ""; }
作用:
用于指定环绕通知。
属性:
value: 用于指定切入点表达式,可以是表达式,也可以是表达式的引用。
argNames: 用于指定切入点表达式参数的名称。它要求和切入点表达式中的参数名称 一致。通常不指定也可以获取切入点方法的参数内容。
使用场景:
环绕通知有别于前面介绍的四种通知类型。它不是指定增强方法执行时机的,而是 spring为我们提供的一种可以通过编码的方式手动控制增强方法何时执行的机制。
/** * 环绕通知 */ @Around("execution(* com.itheima.service.impl.*.*(..))") public Object arountPrintLog(ProceedingJoinPoint pjp){ //1.定义返回值 Object rtValue = null; try{ //前置通知 System.out.println("执行切入点方法前记录日志"); //2.获取方法执行所需的参数 Object[] args = pjp.getArgs(); //3.执行切入点方法 rtValue = pjp.proceed(args); //后置通知 System.out.println("正常执行切入点方法后记录日志"); }catch (Throwable t){ //异常通知 System.out.println("执行切入点方法产生异常后记录日志"); }finally { //最终通知 System.out.println("无论切入点方法执行是否有异常都记录日志"); } return rtValue; }