AOP 面向切面编程

        AOP(Aspect Orient Programming),面向切面编程。面向切面编程是从动态角度考虑程 序运行过程。 AOP 底层,就是采用动态代理模式实现的。采用了两种代理:JDK 的动态代理,与 CGLIB 的动态代理。

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

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

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

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

AOP编程术语

1.切面(Aspect)

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

2.连接点(JoinPoint)

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

3.切入点(Pointcut)

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

4.目标对象(Target)

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

5.通知(Advice)

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

切入点表达式

        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 抛出异常类型

?表示可选的部分

即: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)也是。

实现步骤

 业务接口与实现类

package service;

public interface SomeService {
    void doSome(String name,Integer age);
    String doOther(String name,Integer age);
    String doAround(String name, Integer age);
    void doAfterThrowing();
    void doAfter();
}
package service.impl;

import service.SomeService;

public class SomeServiceImpl implements SomeService {
    public SomeServiceImpl() {
        System.out.println("SomeServiceImpl的无参构造方法");
    }

    @Override
    public void doSome(String name,Integer age) {
        //给doSome()增加功能,在地Some()之前,输出方法的执行时间
        System.out.println("========目标方法doSome()========");
    }

    @Override
    public String doOther(String name, Integer age) {
        System.out.println("========目标方法doOther()========");
        return "doOther";
    }

    @Override
    public String doAround(String name, Integer age) {
        System.out.println("========目标方法doAround()========");
        return "doFirst";
    }

    @Override
    public void doAfterThrowing() {
        System.out.println("========目标方法doAfterThrowing()========"+1/0);
    }

    @Override
    public void doAfter() {
        System.out.println("========目标方法myAfter()========");
        int i = 1/0;
    }
}

1.定义切面类

package service.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;

import java.util.Date;

/**
 * *@Aspect : 是AspectJ框架中的注解
 *      作用:表示当前类是切面类
 *      切面类:是用来给业务方法增加功能的类,在这个类中有切面的功能代码
 *      位置:在类定义的上面
 */
@Aspect
public class MyAspect {
    /**
     * 定义方法,方法是实现切面功能的
     * 方法的定义要求:
     *  1)公共方法public
     *  2)方法没有返回值
     *  3)方法名称自定义
     *  4)方法可以有参数,也可以没有参数
     *      如果有参数,参数不是自定义的,有几个参数类型可以使用
     */

    /**
     * *@Before  前置通知注解
     *    属性:value,是切入点表达式,表示切入点功能执行的位置
     *    位置:在方法的上面
     *  特点:
     *      1.在目标方法执行之前执行
     *      2.不会改变目标方法的执行结果
     *      3.不会影响目标方法的执行
     */
    @Before(value = "execution(public void service.impl.SomeServiceImpl.doSome(String,Integer))")
    public void myBefore01(){
        System.out.println("前置通知,功能:在目标方法执行之前输出执行时间"+new Date());
    }
    /**
     *    *@Before(value = "execution(public void *..SomeServiceImpl.doSome(..))")
     *    *@Before(value = "execution(* *..SomeServiceImpl.do*(..))")
     *    *@Before(value = "execution(* do*(..))")
     *     public void myBefore(){
     *         System.out.println("前置通知,功能:在目标方法执行之前输出执行时间"+new Date());
     *     }
     */

    /**
     * 指定通知方法中的参数: JoinPoint
     * JoinPoint:业务方法,要加入切面功能的业务方法
     *  作用是:可以在通知方法中获取方法执行的信息,例如方法名,方法的实参
     *  如果切面功能中需要用到方法的信息,可以加入JoinPoint
     *  这个JoinPoint参数的值是由框架赋予,必须是第一个位置的参数
     */
    @Before(value = "execution(void *..SomeServiceImpl.doSome(String,Integer))")
    public void myBefore02(JoinPoint joinPoint){
        //获取方法的完整定义
        System.out.println("方法的签名:"+joinPoint.getSignature());
        System.out.println("方法的名称:"+joinPoint.getSignature().getName());
        //获取方法的实参
        Object[] args = joinPoint.getArgs();
        for (Object arg:args) {
            System.out.println("参数:"+arg);
        }
    }

    /**
     * *@AfterReturning  后置通知注解
     *    方法需要有参数,推荐使用Object
     *    属性:1.value,是切入点表达式,表示切入点功能执行的位置
     *        2.returning 自定义的变量,表示目标方法的返回值的
     *    位置:在方法的上面
     *    特点:
     *        1.在目标方法执行之后执行
     *        2.能够获取到目标方法的返回值,可以根据这个返回值做不同的处理
     *              Object res = doOther();
     *        3.可以修改这个返回值
     */
    @AfterReturning(value = "execution(* *..SomeServiceImpl.doOther(..))",
                    returning = "res")
    public void myAfterReturning(Object res){
        //Object res:是目标方法的返回值
        System.out.println("后置通知,返回值是:"+res);
        res = res+"修改";
        System.out.println("后置通知,返回值修改后:"+res);
    }

    /** *@Around 环绕通知
     *  1.public
     *  2.必须有一个返回值,推荐使用Object
     *  3.方法有参数,固定的参数ProceedingJoinPoint
     *  特点:
     *      1.它是功能最强的通知
     *      2.在目标方法的前和后都能增强功能
     *      3.控制目标方法是否被调用执行
     *      4.修改原来的目标方法的执行结果,影响最后的调用结果
     *
     *  环绕通知,等同于jdk动态代理里的,InvocationHandler接口
     *
     *  参数:proceedingJoinPoint 就等同于 Method
     *      作用:指行目标方法的
     *  返回值:就是目标方法的执行结果,可以被修改
     */
    @Around(value = "execution(* *..SomeServiceImpl.doAround(..))")
    public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
        //获取第一个参数值
        String name = "";
        Object[] args = pjp.getArgs();
        if (args!=null&&args.length>1){
            Object arg = args[0];
            name = (String) arg;
        }
        //实现环绕通知
        Object result = null;
        System.out.println("环绕通知,目标方法之前,方法执行时间"+new Date());
        //1.目标方法调用
        result = pjp.proceed();//method.invoke(); Object result = doAround();
        result = result+"修改"+name;
        System.out.println("环绕通知,目标方法之后,方法执行时间"+new Date());
        //返回目标方法的执行结果
        return result;
    }

    /** *@AfterThrowing  异常通知
     *  1.public
     *  2.没有返回值
     *  3.可以没有参数,如果有是JoinPoint
     *  参数:1.value 切入点表达式
     *      2.throwing 自定义的变量,表示目标方法抛出的异常对象
     *                  变量名必须和方法的参数名一样
     *  特点:
     *      1.在目标方法抛出异常时执行
     *      2.可以做异常的监控程序,监控目标方法执行时是不是有异常
     *         如果有异常,可以发送邮件,短信进行通知
     *  执行就是:
     *      try{
     *          SomeServiceImpl.doAfterThrowing();
     *      }catch(Exception e){
     *          myAfterThrowing(e);
     *      }
     */
    @AfterThrowing(value = "execution(* *..SomeServiceImpl.doAfterThrowing())",
                   throwing = "e")
    public void myAfterThrowing(Exception e){
        System.out.println("异常了!"+e.getMessage());
    }

    /** *@After 最终通知
     * 1.public
     * 2.没有返回值
     * 3.方法没有参数,如果有还是JoinP
     * 特点:
     *  1.总是会执行
     *  2.在目标方法之后执行
     */
    @After(value = "execution(* *..SomeServiceImpl.doAfter())")
    public void myAfter(){
        System.out.println("最终通知,无论如何都会执行,就算抛异常");
        //一般做资源清除工作
    }

    /** *@Pointcut : 定义和管理切入点,如果项目中有多个切入点表达式是重复的,可以复用的,使用@Pointcut
     *  属性:value 切入点表达式
     *  位置:自定义方法的上面 方法一般是私有的,因为不需要被外部调用
     *  特点:
     *      当使用@Pointcut定义在一个方法的上面,此时这个方法的名称就是切入点表达式的别名.
     *      其它的通知中,value属性就可以使用这个方法名称,代替切入点表达了
     */
    @Pointcut(value = "execution(* *..SomeServiceImpl.doAfter())")
    public void myPointcut(){
        //无需代码
    }
    @After(value = "myPointcut()")
    private void myPointcutTest(){
        System.out.println("Pointcut的测试");
        //一般做资源清除工作
    }
}

2.配置切面类对象的配置文件

<?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/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--
        声明组件扫描器:component-scan 组件就是java对象
        base-package:指定注解在项目中的包名
            工作方式:spring会扫描遍历指定的包名,把包和子包中所有的类,找到类中的注解
                按照注解的功能创建对象,或给属性赋值
        指定多个包的三种方式:
            第一种:使用多次组件扫描器,指定不同的包
            第二种:使用分隔符(;或,)分隔多个包名
                <context:component-scan base-package="service01;service02"/>
            第三种:直接指定父包
    -->
    <context:component-scan base-package="service"/>
    <!--aspectj:一个开源的专门做aop的框架,spring框架中集成了aspectj框架,通过spring就可以使用aspectj
                aspectj的实现方式有两种:
                1)使用xml的配置文件,配置全局事务
                2)使用注解
    -->
    <!--AspectJ框架的使用
        1)切面的时间,这个执行时间在规范中叫做Advice(通知,增强),
        在AspectJ框架中使用注解表示,也可以使用xml配置文件中的标签
            (1)@Before
            (2)@AfterReturning
            (3)@Around
            (4)@AfterThrowing
            (5)@After
        2)表示切面执行的位置,使用的是切入点的表达式,表达式原型是:
            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  抛出异常类型
            ?  表示可选的部分
            execution(访问权限 方法返回值 方法声明(参数) 异常类型)
            *  0至多个任意字符
            ..  用在参数中,表示任意多个参数,用在包名后,表示当前包及其子包路径
            +  用在类名后,表示当前类及其子类,用在接口后,表示当前接口及其实现类
    -->
    <bean id="someService" class="service.impl.SomeServiceImpl"/>
    <!--声明切面类对象-->
    <bean id="myAspect" class="service.aspect.MyAspect"/>
    <!--声明自动代理生成器:使用AspectJ框架内部的功能,创建目标对象的代理对象
        创建代理对象是在内存中实现的,修改目标对象的内存中的结构,创建为代理对象
        所以目标对象就是被修改后的代理对象
    -->
    <aop:aspectj-autoproxy/>
</beans>

测试

package org.example;

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import service.SomeService;

public class AppTest02 {
    //测试@Before
    @Test
    public void test01(){
        String config = "applicationContext02.xml";
        ApplicationContext ac = new ClassPathXmlApplicationContext(config);
        SomeService service = (SomeService) ac.getBean("someService");
        service.doSome("zs",20);
    }
    //测试@AfterReturning
    @Test
    public void test02(){
        String config = "applicationContext02.xml";
        ApplicationContext ac = new ClassPathXmlApplicationContext(config);
        SomeService service = (SomeService) ac.getBean("someService");
        service.doOther("zs",20);
    }
    //测试@Around
    @Test
    public void test03(){
        String config = "applicationContext02.xml";
        ApplicationContext ac = new ClassPathXmlApplicationContext(config);
        SomeService service = (SomeService) ac.getBean("someService");
        String s = service.doAround("zs",20);
        System.out.println("doAround执行结果:"+s);
    }
    //测试@AfterThrowing
    @Test
    public void test04(){
        String config = "applicationContext02.xml";
        ApplicationContext ac = new ClassPathXmlApplicationContext(config);
        SomeService service = (SomeService) ac.getBean("someService");
        service.doAfterThrowing();
    }
    //测试@After
    @Test
    public void test05(){
        String config = "applicationContext02.xml";
        ApplicationContext ac = new ClassPathXmlApplicationContext(config);
        SomeService service = (SomeService) ac.getBean("someService");
        service.doAfter();
    }
    //测试@Pointcut
    @Test
    public void test06(){
        String config = "applicationContext02.xml";
        ApplicationContext ac = new ClassPathXmlApplicationContext(config);
        SomeService service = (SomeService) ac.getBean("someService");
        service.doAfter();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值