关于java:欢迎光临Spring时代二-上柱国AOP列传

31次阅读

共计 11640 个字符,预计需要花费 30 分钟才能阅读完成。

这里其实讲的是应用如何在 Spring Framework 如何应用 AOP。在看本篇之前,倡议先看代理模式 -AOP 绪论,代理模式 -AOP 绪论是本篇的基础理论,不看也行,本篇讲的也就是讲根本应用。

AOP 简论

AOP: Aspect oriented programming 面向切面编程,是 OOP(面向对象编程)的扩大。
如果说 OOP 是解决面向过程中遇到的一些问题 (比方代码复用性不强等) 的话,那么 AOP 解决的就是通用代码反复编写的问题。
这么说可能有点形象,咱们向事实的场景拉一拉,咱们的办法通常是一步一步执行的,不同的办法有不同的调用机会。

但总有一些办法仿佛是在所有的代码流程外面都须要,比方检测是否登录,记录执行工夫等。然而咱们总不想做反复的事件,就是在每个代码中写一遍,咱们的欲望是写一次,而后标定哪些代码在哪里须要执行呢? AOP 解决的就是这类问题。那么这样做的话,咱们首先就要表明哪里是须要反复执行的,也就是注入点,用 AOP 的术语就是切入点(pointcut),第二个就是申明在切入点在满足哪些机会时注入,比方能够抉择一个办法在执行时注入,在办法产生异样时注入。像 Spring Proxy 并不反对多种多样的连接点,百分之九十九的状况是在办法执行的时候注入。这个工夫咱们用术语称之为连接点(joinPoint)。第三个就是注入的机会,是在办法前还是办法后,还是办法前后都来一遍。这个概念咱们个别称之为倡议(advice)。

pointCut+advice 能够形容一段代码在什么机会注入 (jointPoint),注入到哪里(pointCut),而被注入的代码咱们用切面(Aspect) 这个概念来形容,这也就是面向切面编程 (Aspect oriented programmin)。

认真想一下,下面讲的 AOP 中的倡议,是不是跟动静代理中的在不批改原来类的根底上加强一个类有点像呢? 没错 AOP 是借助于动静代理来实现的,如果你不懂什么是动静代理,请参看这篇文章:

  • 代理模式 -AOP 绪论

其实不懂动静代理,也能看懂本篇。本篇次要介绍的是 AOP 在 Spring 中是如何应用的,AOP 是一种理念,Spring 实现了 AOP。只不过了解动静代理对 Spring 实现的 AOP 会了解更深而已。

筹备工作

在欢迎光临 Spring 时代(一) 上柱国 IOC 列传的依赖咱们还要接着用,同时咱们还要再补充以下依赖:

<dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>5.2.8.RELEASE</version>
        </dependency>
<dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.5</version>
</dependency>

不会 maven 想下 jar 包的,在欢迎光临 Spring 时代(一) 上柱国 IOC 列传中曾经介绍了下载 jar 包的办法,这里不再反复介绍了。当初 Spring Framework 的轻量级曾经体现进去了,依赖很少,大抵上只须要七个 jar 包就能帮忙咱们优雅的治理对象之间的依赖关系和 AOP 了。

AOP 在 Spring 中的实现简介

Java 中办法依附于类,而要应用 Spring 提供的 AOP 也就须要将须要加强的类退出到 IOC 容器中,而在欢迎光临 Spring 时代(一) 上柱国 IOC 列传咱们曾经介绍过了,IOC 容器有两种模式,一种是基于配置文件的,一种是基于注解的。咱们首先来介绍基于配置文件的。在 Spring Framework 中咱们个别称倡议为告诉,在办法执行前执行,咱们称之为前置告诉,在办法执行后执行,咱们称之为后置告诉,在办法执行之前和在办法执行之后都执行咱们称之为盘绕告诉,在办法产生异样的时候执行咱们称之为异样告诉。

在 AOP 中咱们介绍了三个概念:

  • 切点 (pointCut) 形容在哪些办法上执行
  • 连接点(joinPoint) 形容了注入机会
  • 切面(Aspect) 形容的是被注入代码。

留神这三个概念,咱们在用 Spring Framework 提供的 AOP 的时候经常会碰到。

如何让一个一般类中的办法称为一个切面呢? 那必定要让 Spring Framework 辨识到这个办法跟别的办法不一样,Spring Framework 提供了以下接口:

  • org.springframework.aop.MethodBeforeAdvice

前置告诉 在办法执行之前执行

  • org.springframework.aop.AfterReturningAdvice

后置告诉 在办法执行之后执行

  • org.aopalliance.intercept.MethodInterceptor

盘绕告诉 拦挡指标办法的调用,即调用指标办法的整个过程,即能够做到办法执行之前执行、办法执行之后执行、办法产生了异样之后执行

  • org.springframework.aop.ThrowsAdvice

异样告诉 在办法产生了异样之后执行
实现它们,并将它们纳入到 IOC 容器的管辖,再筹备切点,连接点。咱们就能做到 AOP。

咱们下面讲切点是说要加强哪些办法,那办法在 Java 中应该怎么形容呢? 咱们用全类名 + 办法名?这样写起来多麻烦啊!这也就是 execution 表达式入世的起因,简略点,再简略点

execution 表达式简介

格局通常如下:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)

  • modifiers-pattern?: 指定办法的修饰符(public private protected),反对通配符,该局部能够省略
  • ret-type-pattern: 办法的返回值类型,反对通配符,可应用 * 来拦挡所有的返回类型
  • declaring-type-pattern?: 该办法所属的类,即全类名 + 办法名。也能够用 * 拦挡所有办法
  • name-pattern: 指定要匹配的办法名,反对通配符,能够应用 * 通配符来匹配所有办法。
  • param-pattern: 指定办法申明中的形参列表,反对两个通配符,即 * 和..,其中 * 代表一个任意类型的参数,而.. 表零个或多个任意类型的参数。例如,() 匹配一个不承受任何参数的办法,而 (..) 匹配一个承受任意数量参数的办法,(* ) 匹配了一个承受一个任何类型的参数的办法,(*,String)匹配了一个承受两个参数的办法,其中第一个参数是任意类型,第二个参数必须是 String 类型。
  • throws-pattern:指定办法申明抛出的异样,反对通配符,该局部能够省略。

基于配置文件的 AOP

第一种配置文件的 AOP 形式

前置告诉、后置告诉、盘绕告诉

首先咱们实现 MethodBeforeAdvice:

public class LogBefore implements MethodBeforeAdvice{
    @Override
    public void before(Method method, Object[] args, Object o) throws Throwable {System.out.println("在办法之前执行");
    }
}

而后将 LogBefore 退出到 IOC 容器中,形式有很多种,可参看欢迎光临 Spring 时代(一) 上柱国 IOC 列传,这里选取是在配置文件里配置:

<bean id = "logBefore" class="org.example.aop.LogBefore">
</bean>
 <bean id = "landlord" class = "org.example.aop.Landlord">
  </bean>

而后筹备切点,并将切点和切面分割起来,如上文所说,Spring Framework 并没有给咱们提供多少连接点,咱们抉择的就是在办法执行这个机会,之前、之后,还是盘绕。

<!-- 这个配置是开启 AOP 用的, 记得肯定要加 -->
  <aop:aspectj-autoproxy/>
<aop:config>
        <aop:pointcut id = "pointCut" expression = "execution(public void org.example.aop.Landlord.rentHouse())"/>
        <aop:advisor advice-ref="logBefore" pointcut-ref="pointCut"></aop:advisor>
</aop:config>  

所以咱们的切点就是位于 org.example.aop 包下的 Landlord 的 返回值类型为 void 的 rentHouse 办法。<aop:advisor>用于关联切点和切面。advice-ref 指向切面的 id,pointcut-ref 指向的是切点的 id。
咱们筹备一下代码测试一下:

public interface IRentHouse {
    /**
     * 实现该接口的类将可能对外提供房子
     */
    void rentHouse();}
public class Landlord implements IRentHouse {
    @Override
    public void rentHouse() {System.out.println("我向你提供房子.....");
    }
}
 private static void testLogAop() {ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        Landlord landlord = applicationContext.getBean("landlord", Landlord.class);
        landlord.rentHouse();}

运行后果:
我放在配置文件中的不是 org.example.aop.Landlord,为什么我当初取不进去了啊。因为你此时要加强 Landlord 中的 rentHouse 办法,凑巧你实现了接口,那我就用 JDK 的动静代理给你加强啊。你现再在把配置文件中的 AOP 配置去掉,就会发现能胜利运行了,或者 咱们当初换种接值形式,向容器中索取的是 IRentHouse 类型的 Bean 就不会出错了, 代码如下:

  private static void testLogAop() {ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        IRentHouse landlord = applicationContext.getBean("landlord", IRentHouse.class);
        landlord.rentHouse();}

这个例子是我特意筹备的例子,让须要加强的类实现了一个接口,为的就是与代理模式 -AOP 绪论这篇文章造成映照,如果看过这篇文章的同学,就会晓得动静代理还有另一种模式是基于 Cglib 库来做的,这种加强就是在运行时创立须要加强类的子类,所以此时你将下面的 Landlord 不实现 IRentHouse 接口,取 Landlord 类型的就能够了。这其中就有向上转型、向下转型的知识点。能够认真领会一下。

后置告诉和盘绕告诉基本上就是照葫芦画瓢而已,跟前置告诉相似。后置告诉咱们实现 AfterReturningAdvice 接口,盘绕告诉咱们实现 MethodInterceptor 接口:

public class LogAfter implements AfterReturningAdvice {
    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {System.out.println("这是后置告诉...............");
    }
}

public class LogAround implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {System.out.println("这是盘绕告诉, 在办法执行之前执行.........");
        Object result = null;
        try {
         //  invocation 控制目标办法的执行, 正文这段, 则指标办法不会执行。result = invocation.proceed();}catch (Exception ex){System.out.println("这是盘绕告诉, 在办法执行之后执行.........,能够当做异样告诉");
            // ex.printStackTrace();}
        System.out.println("这是盘绕告诉, 在办法执行之后执行.........");
        return result;
    }
}

在配置文件中配置:

 <bean id = "logAfter" class="org.example.aop.LogAfter">
 </bean>
 <bean id = "logAround" class = "org.example.aop.LogAround">
 </bean>
<aop:config>
        <aop:pointcut id = "pointCut" expression = "execution(public void org.example.aop.Landlord.rentHouse())"/>
        <aop:advisor advice-ref="logAround" pointcut-ref="pointCut"></aop:advisor>
    </aop:config>
    <aop:config>
        <aop:pointcut id = "pointCut" expression = "execution(public void org.example.aop.Landlord.rentHouse())"/>
        <aop:advisor advice-ref="logException" pointcut-ref="pointCut"></aop:advisor>
    </aop:config>
    <aop:config>
        <aop:pointcut id = "pointCut" expression = "execution(public void org.example.aop.Landlord.rentHouse())"/>
        <aop:advisor advice-ref="logBefore" pointcut-ref="pointCut"></aop:advisor>
    </aop:config>
    <aop:config>
        <aop:pointcut id = "pointCut" expression = "execution(public void org.example.aop.Landlord.rentHouse())"/>
        <aop:advisor advice-ref="logAfter" pointcut-ref="pointCut"></aop:advisor>
    </aop:config>

测试代码:

private static void testLogAop() {ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        Landlord landlord = applicationContext.getBean("landlord", Landlord.class);
        landlord.rentHouse();}

运行后果:

异样告诉

异样告诉跟其余接口的不一样之处,就在于这是一个空接口:

那么在办法在执行过程中,产生了异样,Spring 该怎么回调这个办法呢? 别着急咱们先看正文:

问题解决了, 咱们照正文要求的来:

public class LogException implements ThrowsAdvice {public void afterThrowing(Method method, Object[] args, Object target, Exception ex){System.out.println(method.getName()+"办法产生了异样");
    }
}

而后再度测试,还是下面的测试代码,在指标代码中请做出一个 Exception 的子类,我做的是除零异样:

第二种配置文件的 AOP 形式

下面实现的各种告诉都是基于接口的,如果你不想实现接口,Spring 也能将一个一般的类变成告诉类,写到这里有想到了动静代理,想必还是基于动静代理的第二种模式来做的。咱们首先筹备一个告诉类:

@Component
public class LogSchema {public void before(JoinPoint joinPoint) {System.out.println("z 办法调用执行之前执行");
    }
    public void invoke(ProceedingJoinPoint joinPoint) throws Throwable {System.out.println("z 盘绕告诉的前置告诉........");
        Object result = null;
        try {
            // 管制办法的应用
            result = joinPoint.proceed();
            System.out.println("z 打印办法的返回值..........." + result);
        } catch (Exception e) {System.out.println("z 盘绕告诉的异样告诉........");
        }
        System.out.println("z 盘绕告诉的后置告诉........");
    }
    public void after(JoinPoint joinPoint, Object returningValue) {System.out.println("z 办法的返回值是:" + returningValue);
        System.out.println("z 办法调用执行之后执行");
    }
    public void afterThrowing(JoinPoint joinPoint, ArithmeticException ex) {System.out.println("z 这是异样告诉的告诉");
    }
}

而后将这个类在配置文件变成告诉类:

<aop:config>
        <aop:pointcut id="schema" expression="execution(public void org.example.aop.Landlord.rentHouse(int))"/>
        <aop:aspect ref="logSchema">
            <aop:around method = "invoke" pointcut-ref="schema" arg-names="joinPoint"/>
            <aop:before method = "before" pointcut-ref="schema"  arg-names="joinPoint"/>
            <aop:after-returning method="after"  returning="returningValue" arg-names="joinPoint,returningValue" pointcut-ref="schema"/>
            <aop:after-throwing method="afterThrowing" pointcut-ref="schema" arg-names="joinPoint,ex" throwing="ex"/>
        </aop:aspect>
    </aop:config>

测试代码:

基于注解的 AOP

接下来咱们介绍基于注解模式的 Spring AOP,对于注解其实也有几种不同的写法,次要区别在于取参数的形式不同。一种是在注解中绑定参数,一种是用 JoinPoint 对象获取参数。两种咱们都介绍,在应用注解的形式开发 AOP 之前,咱们首先要 Spring 对 AOP 的反对,如果你是配置文件请在配置文件中加上: <aop:aspectj-autoproxy/>, 如果是配置类请在配置类上加上 @EnableAspectJAutoProxy 注解。不然 Spring 就认为你不想用 AOP。

基于 @Pointcut 的写法

取指标办法参数的第一种模式

@Aspect
// 该注解将该类纳入 IOC 容器
@Component 
public class LogAspectAnnotation {

    /**
     * Pointcut 注解用于定义切点, 能够被其余办法援用。* 相当于配置文件的: <aop:pointcut id = "pointCut" expression = "execution(public void    &org.example.aop.Landlord.rentHouse())"/>
     * Pointcut 有两个属性一个是 value 用于指定 execution 表达式,argNames 用于匹配参数取参数。* 记得语法是跟在配置文件中不同的是写完 execution 表达式, 用 &&args 写参数名, 多的用逗号隔开, 在 argNames 中写对应的参数名
     * 而后在切点的办法中也要写, 用于绑定参数。* @param i
     * @return
     */
    @Pointcut(value = "execution(public void org.example.aop.Landlord.rentHouse(int)) && args(i)",argNames = "i")
    public int  pointCut(int i){return i;}
    @Before(value = "pointCut(i)",argNames = "i")
    public void before(int i){System.out.println("办法调用执行之前执行");
    }
}

取指标办法参数的第二种模式

@Pointcut(value = "execution(public void org.example.aop.Landlord.rentHouse(int))")
    public void  pointCut(){}
    @Before(value = "pointCut()")
    public void before(JoinPoint joinPoint){for (Object arg : joinPoint.getArgs()) {System.out.println(arg);
        }
        System.out.println("办法调用执行之前执行");
    }

JoinPoint 能够获取指标类的所有元数据,咱们来大抵看一下 JoinPoint 类:

其实还能够这么写

// value 就间接相当于切点
@Before(value = "execution(public void org.example.aop.Landlord.rentHouse(int))")
    public void before(JoinPoint joinPoint){
        // 这里是打印切点的参数
        for (Object arg : joinPoint.getArgs()) {System.out.println(arg);
        }
        System.out.println("办法调用执行之前执行");
    }

后置告诉、盘绕告诉、异样告诉

@Aspect
@Component
public class LogAspectAnnotation {

    /**
     * Pointcut 注解用于定义切点, 能够被其余办法援用。* 相当于配置文件的: <aop:pointcut id = "pointCut" expression = "execution(public void org.example.aop.Landlord.rentHouse())"/>
     * Pointcut 有两个属性一个是 value 用于指定 execution 表达式,argNames 用于匹配参数取参数。* 记得语法是跟在配置文件中不同的是写完 execution 表达式, 用 &&args 写参数名, 多的用逗号隔开, 在 argNames 中写对应的参数名
     * 而后在切点的办法中也要写, 用于绑定参数。*
     * @param
     * @return
     */
    @Pointcut(value = "execution(public void org.example.aop.Landlord.rentHouse(int))")
    public void pointCut() {}

    @Before(value = "pointCut()")
    public void before(JoinPoint joinPoint) {System.out.println("办法调用执行之前执行");
    }

    /**
     * 绝对于前置告诉,AfterReturning 中多了一个属性就是 returning, 用于获取指标办法的返回值。* returning 中的值要和后置告诉的参数名保持一致
     *
     * @param joinPoint
     * @param returningValue
     */
    @AfterReturning(value = "execution(public void org.example.aop.Landlord.rentHouse(int))", returning = "returningValue")
    public void after(JoinPoint joinPoint, Object returningValue) {System.out.println("办法的返回值是:" + returningValue);
        System.out.println("办法调用执行之后执行");
    }

    /**
     * 盘绕告诉用 ProceedingJoinPoint 来管制办法的执行
     *
     * @param joinPoint
     * @throws Throwable
     */
    @Around("execution(public void org.example.aop.Landlord.rentHouse(int))")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {System.out.println("盘绕告诉的前置告诉........");
        Object result = null;
        try {
            // 管制办法的应用
            result = joinPoint.proceed();
            System.out.println("打印办法的返回值..........." + result);
        } catch (Exception e) {System.out.println("盘绕告诉的异样告诉........");
        }
        System.out.println("盘绕告诉的后置告诉........");
    }

    /**
     * throwing 属性会将产生异样时的对象传递给办法的参数, 所以 throwing 的属性值和参数名要保持一致
     * 产生了办法中的异样就会触发异样告诉, 以后办法就是 ArithmeticException 时触发
     *
     * @param joinPoint
     * @param ex
     */
    @AfterThrowing(value = "execution(public void org.example.aop.Landlord.rentHouse(int))", throwing = "ex")
    public void afterThrowing(JoinPoint joinPoint, ArithmeticException ex) {System.out.println("这是异样告诉的告诉");
        ex.printStackTrace();}
}

还是下面的测试代码,我在 Landlord 的 rentHouse 办法中做了一个 1 除以 0 操作,咱们来看一下测试后果:

总结一下

之前我学 Spring Framework 的时候就是看的颜群老师在 B 站公布的视频,这个教程也的确不错,过后学的时候心里还存在些疑难,比方 AOP 的实现,IOC 到底做了什么,打算当前有工夫将这些疑难全副解决等。到当初这些问题算上之前的三篇博客大多都得以解决,这也算是从新梳理了一下本人对 Spring Framework 的意识,因为我还是心愿本人的常识成零碎一点,不是零零碎碎的。心愿对大家学习 Spring Framework 有所帮忙。

参考资料

  • 如何用通俗易懂形式解释面向(切面)AOP 编程 和 AOP 的实现形式?
  • Java 进阶常识 23 Spring execution 切入点表达式
  • Spring 视频教程 颜群

正文完
 0