ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Spring学习笔记三

2021-02-10 16:04:48  阅读:36  来源: 互联网

标签:.. Spring 切入点 笔记 学习 切面 execution 方法 public


AOP 面向切面编程

1、不使用 AOP 的开发方式【理解】

1.1 Demo1

先定义好接口与一个实现类,该实现类中除了要实现接口中的方法外,还要再写两个非业务方法。非业务方法也称为交叉业务逻辑:
doTransaction():用于事务处理
doLog():用于日志处理
然后,再使接口方法调用它们。接口方法也称为主业务逻辑。

接口:
在这里插入图片描述

public class SomeServiceImpl implements SomeService {

    @Override
    public void doSome() {
        doLog();
        System.out.println("执行业务方法doSome()");
        doTrans();
    }

    @Override
    public void doOther() {
        doLog();
        System.out.println("执行业务方法doOther()");
        doTrans();
    }

    public void doLog(){
        System.out.println("非业务功能,日志功能,在方法开始时输出日志");
    }

    public void doTrans(){
        System.out.println("非业务事务功能,在业务方法执行之后,加入事务");
    }
}

1.2 Demo2

当然,也可以有另一种解决方案:将这些交叉业务逻辑代码放到专门的工具类或处理类中,由主业务逻辑调用。

public class ServiceTools {
    public static void doLog(){
        System.out.println("非业务功能,日志功能,在方法开始时输出日志");
    }

    public static void doTrans(){
        System.out.println("非业务事务功能,在业务方法执行之后,加入事务");
    }
}
public class SomeServiceImpl implements SomeService {

    @Override
    public void doSome() {
        ServiceTools.doLog();
        System.out.println("执行业务方法doSome()");
        ServiceTools.doTrans();
    }

    @Override
    public void doOther() {
        ServiceTools.doLog();
        System.out.println("执行业务方法doOther()");
        ServiceTools.doTrans();
    }

}

1.3 Demo3

以上的解决方案,还是存在弊端:交叉业务与主业务深度耦合在一起。当交叉业务逻辑较多时,在主业务代码中会出现大量的交叉业务逻辑代码调用语句,大大影响了主业务逻辑的可读性,降低了代码的可维护性,同时也增加了开发难度。

所以,可以采用动态代理方式。在不修改主业务逻辑的前提下,扩展和增强其功能。

功能增强:

public class MyInvocationHandler implements InvocationHandler {

    private Object target;

    public MyInvocationHandler() {
    }

    public MyInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object obj = null;
        //在方法之前输出日志
        ServiceTools.doLog();
        //执行目标方法,执行target对象的方法
        obj = method.invoke(target, args);
        //在方法执行后,执行事务
        ServiceTools.doTrans();
        //目标方法的执行结果
        return obj;
    }
}
    @Test
    public void test01() {
        //创建代理对象
        SomeService target = new SomeServiceImpl();
        MyInvocationHandler handler = new MyInvocationHandler(target);
        SomeService proxy = (SomeService) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                handler
        );
        //通过代理对象执行业务方法,实现日志,事务的增强
        proxy.doSome();
        proxy.doOther();
    }

在这里插入图片描述

2、AOP 简介

AOP(Aspect Orient Programming),面向切面编程。面向切面编程是从动态角度考虑程序运行过程。

AOP 底层,就是采用动态代理模式实现的。采用了两种代理:JDK 的动态代理,与 CGLIB的动态代理。

AOP 为 Aspect Oriented Programming 的缩写,意为:面向切面编程,可通过运行期动态代理实现程序功能的统一维护的一种技术。AOP 是 Spring 框架中的一个重要内容。利用 AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

面向切面编程,就是将交叉业务逻辑封装成切面,利用 AOP 容器的功能将切面织入到主业务逻辑中。所谓交叉业务逻辑是指,通用的、与主业务逻辑无关的代码,如安全检查、事务、日志、缓存等。

若不使用 AOP,则会出现代码纠缠,即交叉业务逻辑与主业务逻辑混合在一起。这样,会使主业务逻辑变的混杂不清。

例如,转账,在真正转账业务逻辑前后,需要权限控制、日志记录、加载事务、结束事务等交叉业务逻辑,而这些业务逻辑与主业务逻辑间并无直接关系。但,它们的代码量所占比重能达到总代码量的一半甚至还多。它们的存在,不仅产生了大量的“冗余”代码,还大大干扰了主业务逻辑—转账。

3、面向切面编程对有什么好处?

  1. 减少重复;
  2. 专注业务;
    注意:面向切面编程只是面向对象编程的一种补充。

使用 AOP 减少重复代码,专注业务实现:
在这里插入图片描述

4、AOP 编程术语【掌握】

1) 切面(Aspect)

切面泛指交叉业务逻辑。上例中的事务处理、日志处理就可以理解为切面。常用的切面是通知(Advice)。实际就是对主业务逻辑的一种增强。

2) 连接点(JoinPoint)

连接点指可以被切面织入的具体方法。通常业务接口中的方法均为连接点。

3) 切入点(Pointcut)

切入点指声明的一个或多个连接点的集合。通过切入点指定一组方法。
被标记为 final 的方法是不能作为连接点与切入点的。因为最终的是不能被修改的,不能被增强的。

4) 目标对象(Target)

目 标 对 象 指 将 要 被 增 强 的 对 象 。 即 包 含 主 业 务 逻 辑 的 类 的 对 象 。 上 例 中 的StudentServiceImpl 的对象若被增强,则该类称为目标类,该类对象称为目标对象。当然,不被增强,也就无所谓目标不目标了。

5) 通知(Advice)

通知表示切面的执行时间,Advice 也叫增强。上例中的 MyInvocationHandler 就可以理
解为是一种通知。换个角度来说,通知定义了增强代码切入到目标代码的时间点,是目标方
法执行之前执行,还是之后执行等。通知类型不同,切入时间不同。
切入点定义切入的位置,通知定义切入的时间。

5、AspectJ 对 AOP 的实现【掌握】

对于 AOP 这种编程思想,很多框架都进行了实现。Spring 就是其中之一,可以完成面向切面编程。然而,AspectJ 也实现了 AOP 的功能,且其实现方式更为简捷,使用更为方便,而且还支持注解式开发。所以,Spring 又将 AspectJ 的对于 AOP 的实现也引入到了自己的框架中。

在 Spring 中使用 AOP 开发时,一般使用 AspectJ 的实现方式。

AspectJ 简介:
AspectJ 是一个优秀面向切面的框架,它扩展了 Java 语言,提供了强大的切面实现。
AspectJ官网

5.1 AspectJ 的通知类型【理解】

AspectJ 中常用的通知有五种类型:
(1)前置通知
(2)后置通知
(3)环绕通知
(4)异常通知
(5)最终通知

5.2 AspectJ 的切入点表达式【掌握】

AspectJ 定义了专门的表达式用于指定切入点。表达式的原型是:

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

modifiers-pattern] 访问权限类型
ret-type-pattern 返回值类型
declaring-type-pattern 包名类名
name-pattern(param-pattern) 方法名(参数类型和参数个数)
throws-pattern 抛出异常类型
表示可选的部分

以上表达式共 4 个部分。
execution(访问权限 方法返回值 方法声明(参数) 异常类型)

切入点表达式要匹配的对象就是目标方法的方法名。所以,execution 表达式中明显就是方法的签名。注意,表达式中黑色文字表示可省略部分,各部分间用空格分开。在其中可以使用以下符号:
在这里插入图片描述
举例:
execution(public * *(..))
指定切入点为:任意公共方法。
execution(* set*(..))
指定切入点为:任何一个以“set”开始的方法。
execution(* com.xyz.service.*.*(..))
指定切入点为:定义在 service 包里的任意类的任意方法。
execution(* com.xyz.service..*.*(..))
指定切入点为:定义在 service 包或者子包里的任意类的任意方法。“…”出现在类名中时,后面必须跟“*”,表示包、子包下的所有类。
execution(* *..service.*.*(..))
指定所有包下的 serivce 子包下所有类(接口)中所有方法为切入点
execution(* *.service.*.*(..))
指定只有一级包下的 serivce 子包下所有类(接口)中所有方法为切入点
execution(* *.ISomeService.*(..))
指定只有一级包下的 ISomeSerivce 接口中所有方法为切入点

execution(* *..ISomeService.*(..))
指定所有包下的 ISomeSerivce 接口中所有方法为切入点
execution(* com.xyz.service.IAccountService.*(..))
指定切入点为:IAccountService 接口中的任意方法。
execution(* com.xyz.service.IAccountService+.*(..))
指定切入点为:IAccountService 若为接口,则为接口中的任意方法及其所有实现类中的任意方法;若为类,则为该类及其子类中的任意方法。
execution(* joke(String,int)))
指定切入点为:所有的 joke(String,int)方法,且 joke()方法的第一个参数是 String,第二个参数是 int。如果方法中的参数类型是 java.lang 包下的类,可以直接使用类名,否则必须使用全限定类名,如 joke( java.util.List, int)。
execution(* joke(String,*)))
指定切入点为:所有的 joke()方法,该方法第一个参数为 String,第二个参数可以是任意类型,如joke(String s1,String s2)和joke(String s1,double d2)都是,但joke(String s1,double d2,String s3)不是。
execution(* joke(String,..)))
指定切入点为:所有的 joke()方法,该方法第一个参数为 String,后面可以有任意个参数且参数类型不限,如 joke(String s1)、joke(String s1,String s2)和 joke(String s1,double d2,String s3)都是。
execution(* joke(Object))
指定切入点为:所有的 joke()方法,方法拥有一个参数,且参数是 Object 类型。joke(Object ob)是,但,joke(String s)与 joke(User u)均不是。
execution(* joke(Object+)))
指定切入点为:所有的 joke()方法,方法拥有一个参数,且参数是 Object 类型或该类的子类。不仅 joke(Object ob)是,joke(String s)和 joke(User u)也是。

5.3 AspectJ 的开发环境【掌握】

1) maven 依赖

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.2.5.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aspects</artifactId>
      <version>5.2.5.RELEASE</version>
    </dependency>

2) 引入 AOP 约束

在 AspectJ 实现 AOP 时,要引入 AOP 的约束。配置文件中使用的 AOP 约束中的标签,均是 AspectJ 框架使用的,而非 Spring 框架本身在实现 AOP 时使用的。

AspectJ 对于 AOP 的实现有注解和配置文件两种方式,常用是注解方式。

5.4 AspectJ 基于注解的 AOP 实现【掌握】

AspectJ 提供了以注解方式对于 AOP 的实现。

1) 实现步骤

A、定义业务接口与实现类
public interface SomeService {
    void doSome(String name,Integer age);
}
public class SomeServiceImpl implements SomeService {
    @Override
    public void doSome(String name, Integer age) {
        System.out.println("执行了业务方法doSome");
    }
}
@Aspect
public class MyAspect {
    /**
     * @Before:前置通知
     *  属性:value 切入点表达式,表示切面执行的位置
     */
    @Before(value = "execution(* com.b0kuwa.aspectj..SomeServiceImpl.doSome(..))")
    public void myBefore(){
        System.out.println("前置通知:在目标方法之前先执行,例如输出日志");
    }
}
B、定义切面类

类中定义了若干普通方法,将作为不同的通知方法,用来增强功能。

/**
 * @Aspect:是aspectj框架的注解,表示当前类是切面类
 */
@Aspect
public class MyAspect {
    /**
     * @Before:前置通知
     *  属性:value 切入点表达式,表示切面执行的位置
     */
    @Before(value = "execution(* com.b0kuwa.aspectj..SomeServiceImpl.doSome(..))")
    public void myBefore(){
        System.out.println("前置通知:在目标方法之前先执行,例如输出日志");
    }
}
C、声明目标对象切面类对象
    <!--声明目标类对象-->
    <bean id="someService" class="com.b0kuwa.aspectj.service.impl.SomeServiceImpl"/>
    <!--声明切面类对象-->
    <bean id="myAspect" class="com.b0kuwa.aspectj.advice.MyAspect"/>
D、注册 AspectJ 的自动代理

在定义好切面 Aspect 后,需要通知 Spring 容器,让容器生成“目标类+ 切面”的代理对象。这个代理是由容器自动生成的。只需要在 Spring 配置文件中注册一个基于 aspectj 的自动代理生成器,其就会自动扫描到@Aspect 注解,并按通知类型与切入点,将其织入,并生成代理。

    <!--声明自动代理生成器,创建代理-->
    <aop:aspectj-autoproxy/>

<aop:aspectj-autoproxy/>的底层是由 AnnotationAwareAspectJAutoProxyCreator 实现的。从其类名就可看出,是基于 AspectJ 的注解适配自动代理生成器。

其工作原理是,<aop:aspectj-autoproxy/>通过扫描找到@Aspect 定义的切面类,再由切面类根据切入点找到目标类的目标方法,再由通知类型找到切入的时间点。

E、测试类中使用目标对象的 id
    @Test
    public void test01(){
        String config = "applicationContext.xml";
        ApplicationContext ac = new ClassPathXmlApplicationContext(config);
        //从spring容器中获取目标对象,目标就是经过aspectj修改后的代理对象
        SomeService proxy = (SomeService) ac.getBean("someService");
        //proxy:com.sun.proxy.$Proxy8:目标对象有接口使用jdk动态代理
        System.out.println("proxy:" + proxy.getClass().getName());
        //通过代理执行业务方法,实现功能增强
        proxy.doSome("张三",18);
    }

在这里插入图片描述

2)@Before 前置通知-方法有 JoinPoint 参数【掌握】

在目标方法执行之前执行。被注解为前置通知的方法,可以包含一个 JoinPoint 类型参数。该类型的对象本身就是切入点表达式。通过该参数,可获取切入点表达式、方法签名、目标对象等。

不光前置通知的方法,可以包含一个 JoinPoint 类型参数,所有的通知方法均可包含该参数。

    /**
     * 通知方法:使用了通知注解修饰的方法
     * 通知方法可以有参数,但是参数不是任意
     * @param joinPoint :表示连接点方法
     */
    @Before(value = "execution(* *..SomeServiceImpl.do*(..))")
    public void myBefore2(JoinPoint joinPoint) {
        //JoinPoint能够获取到方法的定义,方法的参数等信息
        System.out.println("连接点的方法定义:"+joinPoint.getSignature());
        System.out.println("连接点方法的参数个数:"+joinPoint.getArgs().length);
        //方法参数的信息
        Object[] args = joinPoint.getArgs();
        for (Object arg : args) {
            System.out.println(arg);
        }
        System.out.println("前置通知:在目标方法之前先执行,例如输出日志");
    }

在这里插入图片描述

3) @AfterReturning 后置通知-注解有 returning 属性【掌握】

在目标方法执行之后执行。由于是目标方法之后执行,所以可以获取到目标方法的返回值。该注解的 returning 属性就是用于指定接收方法返回值的变量名的。所以,被注解为后置通知的方法,除了可以包含 JoinPoint 参数外,还可以包含用于接收返回值的变量。该变量最好为 Object 类型,因为目标方法的返回值可能是任何类型。

接口增加方法:
在这里插入图片描述
实现方法:
在这里插入图片描述
定义切面:

    @AfterReturning(value = "execution(* *..SomeServiceImpl.doOther(..))",returning = "result")
    public void myAfterReturning(Object result) {
        if (result != null) {
            String str = (String) result;
            result = str.toUpperCase();
        }
        System.out.println("后置通知:在目标方法之后执行的功能增强,例如执行事务处理(切面)" + result);
    }

测试:

    @Test
    public void doOther() {
        String config = "applicationContext.xml";
        ApplicationContext ac = new ClassPathXmlApplicationContext(config);
        SomeService proxy = (SomeService) ac.getBean("someService");
        proxy.doOther("李四", 19);
    }

在这里插入图片描述

4)@Around 环绕通知-增强方法有 ProceedingJoinPoint参数 【掌握】

在目标方法执行之前之后执行。被注解为环绕增强的方法要有返回值,Object 类型。并且方法可以包含一个 ProceedingJoinPoint 类型的参数。接口 ProceedingJoinPoint 其有一个proceed()方法,用于执行目标方法。若目标方法有返回值,则该方法的返回值就是目标方法的返回值。最后,环绕增强方法将其返回值返回。该增强方法实际是拦截了目标方法的执行。

接口增加方法:
在这里插入图片描述
接口方法的实现:
在这里插入图片描述
定义切面:

    @Around(value = "execution(* *..SomeServiceImpl.doFirst(..))")
    public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
        Object obj = null;
        //增强功能
        System.out.println("环绕通知:在目标方法之前执行的,例如输出日志");
        //执行目标方法的调用,等同于method.invoke(target,args)
        obj = pjp.proceed();
        //增强功能
        System.out.println("环绕通知:在目标方法之后执行的,例如处理事务");
        //返回目标方法的执行结果
        return obj;
    }

在这里插入图片描述

5)@AfterThrowing 异常通知-注解中有 throwing 属性 【了解】

在目标方法抛出异常后执行。该注解的 throwing 属性用于指定所发生的异常类对象。当然,被注解为异常通知的方法可以包含一个参数 Throwable,参数名称为 throwing 指定的名称,表示发生的异常对象。

增加业务方法:
在这里插入图片描述
方法实现:
在这里插入图片描述
定义切面:

    /**
     * @AfterThrowing:异常通知
     * @param ex
     */
    @AfterThrowing(value = "execution(* *..SomeServiceImpl.doSecond(..))",throwing = "ex")
    public void myAfterThrowing(Throwable ex){
        //把异常发生的时间,位置,原因记录到数据库,日志文件等等
        //可以把异常发生的时间,把异常消息通过短信,邮件发送给开发人员
        System.out.println("异常通知:在目标方法抛出异常时执行的,异常原因:"+ex.getMessage());
    }

6)@After 最终通知【了解】

无论目标方法是否抛出异常,该增强均会被执行。
增加方法:
在这里插入图片描述
方法实现:
在这里插入图片描述
定义切面:

    @After(value = "execution(* *..SomeServiceImpl.doThird(..))")
    public void myAfter(){
        System.out.println("最终通知:总是会被执行的方法");
    }

在这里插入图片描述

7) @Pointcut 定义切入点

当较多的通知增强方法使用相同的 execution 切入点表达式时,编写、维护均较为麻烦。AspectJ 提供了@Pointcut 注解,用于定义 execution 切入点表达式。

其用法是,将@Pointcut 注解在一个方法之上,以后所有的 execution 的 value 属性值均可使用该方法名作为切入点。代表的就是@Pointcut 定义的切入点。这个使用@Pointcut 注解的方法一般使用 private 的标识方法,即没有实际作用的方法。

    @After(value = "pt()")
    public void myAfter(){
        System.out.println("最终通知:总是会被执行的方法");
    }

    /**
     * @Pointcut:用来定义和管理切面点。简化切入点的定义。
     */
    @Pointcut(value = "execution(* *..SomeServiceImpl.doThird(..))")
    public void pt(){

    }

标签:..,Spring,切入点,笔记,学习,切面,execution,方法,public
来源: https://blog.csdn.net/beaiyy/article/details/113775392

专注分享技术,共同学习,共同进步。侵权联系[admin#icode9.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有