SpringAOP深入理解+特别术语理解


之前写过一篇Spring面向切面编程的具体操作:三种方式配置通知,当然也只是停留在操作层面,今天回头看这个知识点的时候,发现自己的理解更加深刻,故在此做一点小小的总结。

AOP面向切面编程是spring的核心之一,它的一些术语还是比较抽象的,至少初始的时候我是这么觉得的,但慢慢接触了一些设计思想,如代理模式创建实现相同接口的代理对象,以增强指定方法的思想之后,就渐渐理解其中的精妙,当然,理解还是不能完全理解的,只能说慢慢探索,日益精进。

一、简单案例的理解

面向切面编程的思想被广泛应用一定有他的道理,一定是因为它的出现解决了某些繁杂的类似于搬砖似的工作。

我们以一个简单案例作为切入,请暂时不要在意其中逻辑,暂时以打印日志信息作为事务控制:

首先,我们定义一个账户接口AccountService,里面包含一些基本的增删改方法,并创建一个实现类AccountServiceImpl实现之,暂且以打印信息模拟数据库操作。

@Service("accountService")
public class AccountServiceImpl implements AccountService {
    public void saveAccount() {
        System.out.println("==> 正常业务:AccountServiceImpl的saveAccount方法正常执行");
    }
    public void updateAccount(int i) {
        System.out.println("==> 正常业务:AccountServiceImpl的updateAccount方法正常执行");
    }
    public int deleteAccount() {
        System.out.println("==> 正常业务:AccountServiceImpl的deleteAccount方法正常执行");
        return 10;
    }
}

需求:在每个方法执行前后都打印日志信息,如果发生异常,打印异常信息。

呃,需求还是很好实现的,随便一想就有俩可以实现这个简单的需求:

  • 直接在方法里面打印信息嘛,所有方法都写上一遍,不怕累,但日志代码大量侵入正常业务功能模块,存在大量耦合,显然不可取。
  • 使用动态代理技术,基于JDK的动态代理技术,创建出与被代理对象实现相同接口的代理对象,在反射调用方法前后对方法进行增强,比如打印必要的日志信息。

于是我们果断采用动态代理的技术,对需求进行实现,并进行了测试:

public class aopTest {
    public static void main(String[] args) {
        //获取容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //获取对象
        final AccountService as = ac.getBean(AccountService.class);
        AccountService asProxy = (AccountService)Proxy.newProxyInstance(as.getClass().getClassLoader(), as.getClass().getInterfaces(), new InvocationHandler() {
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Object value = null;
                //获取方法名
                String name = method.getName();
                try {
                    System.out.println(name+"方法 ==>即将执行...");
                    value = method.invoke(as, args);
                    System.out.println(name+"方法 ==>环绕返回通知... 返回结果 ==>"+value);
                } catch (Throwable e) {
                    System.out.println(name+"方法 ==>环绕异常通知... 异常信息 ==>"+e);
                } finally {
                    System.out.println(name+"方法 ==>最终执行完毕...");
                }
                return value;
            }
        });
        //执行方法
        asProxy.deleteAccount();
        System.out.println("================");
        asProxy.saveAccount();
    }
}

在这里插入图片描述

可以发现动态代理可以实现我们的需求,但JDK的动态代理只能基于接口进行,如果要基于实现类,可以利用第三方库cglib实现,在此就不赘述了。

ok,说到这,我们成功地使日志代码动态地在目标业务方法的前后执行,我们的业务代码仅仅只需要关注业务自身逻辑,而日志信息,事务控制等代码转移至切面中即可,其中的合理性也是显而易见的。

在这里插入图片描述

二、SpringAOP的简单构建

spring框架对AOP的支持构建在动态代理的基础之上,当然也只是支持仅限于方法的拦截。那么,如何来构建呢,关于构建,我在上一篇基于操作的文章中已经写明,这边就选择其中一种,基于xml+注解的方式

一、首先引入必要的jar包座标:

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.4.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.13</version>
        </dependency>

二、定义切面类@Aspect注解标注,并让spring管理,定义通知和切点:

ps:后置通知和返回通知中文翻译上可能会有偏差,以英文语义为准。

/**
 * @author Summerday
 *
 * 记录日志工具类(切面类)
 */
@Component
@Aspect
public class Logger {
    //提取可重用切入点表达式
    @Pointcut("execution(* com.smday.service.impl.*.*(..))")
    private void pt1(){}
    /**
     * 用于打印日志:计划让其在切入点方法执行之前执行(切入点方法就是业务层方法)
     * 可以通过JoinPoint获取目标方法的详细信息
     */

    @Before("pt1()")
    public void printBeforeLog(JoinPoint joinPoint){
        //目标方法运行时的参数
        Object[] args = joinPoint.getArgs();
        //获取方法签名
        String name = joinPoint.getSignature().getName();
        System.out.println(name+"方法 ==>前置通知...");
    }

    @After("pt1()")
    public void printAfterLog(JoinPoint joinPoint){
        //获取方法签名
        String name = joinPoint.getSignature().getName();
        System.out.println(name+"方法 ==>后置通知...");
    }

    //可以指定返回值
    @AfterReturning(value = "pt1()",returning = "result")
    public void printAfterReturningLog(JoinPoint joinPoint,Object result){
        //获取方法签名
        String name = joinPoint.getSignature().getName();
        System.out.println(name+"方法 ==>返回通知... 返回结果 ==>"+result);
    }

    //可以指定异常
    @AfterThrowing(value = "pt1()",throwing = "e")
    public void printAfterThrowingLog(JoinPoint joinPoint,Exception e){
        //获取方法签名
        String name = joinPoint.getSignature().getName();
        System.out.println(name+"方法 ==>异常通知... 异常信息 ==>"+e.getCause());
    }
    //@Around("pt1()")
    //只有环绕通知可以接收ProceedingJoinPoint,而其他通知只能接收JoinPoint
    public Object printAroundLog(ProceedingJoinPoint pjp){
        //获取参数
        Object[] args = pjp.getArgs();
        //获取方法名
        String name = pjp.getSignature().getName();
        Object proceed = null;
        try {
            System.out.println(name+"方法 ==>环绕前置通知...");
            //利用反射推进目标方法即可,即method.invoke(obj,args)
            proceed = pjp.proceed(args);
            System.out.println(name+"方法 ==>环绕返回通知... 返回结果 ==>"+proceed);
        } catch (Throwable throwable) {
            System.out.println(name+"方法 ==>环绕异常通知... 异常信息 ==>"+throwable);
        } finally {
            System.out.println(name+"方法 ==>环绕后置通知...");
        }
        //反射调用后的返回值一定返回出去
        return proceed;
    }
}

三、基于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"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       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 https://www.springframework.org/schema/context/spring-context.xsd">
    <context:component-scan base-package="com.smday"/>
    <!-- 启用AspectJ自动代理-->
    <aop:aspectj-autoproxy/>
</beans>

四、测试通知

public class aopTest {
    public static void main(String[] args) {
        //获取容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //获取对象
        AccountService as = ac.getBean(AccountService.class);
        //执行方法
        as.deleteAccount();
        System.out.println("================");
        as.saveAccount();
    }
}

在这里插入图片描述

我们可以发现执行的顺序:依次为前置通知、方法正常执行、后置通知、返回通知。

三、AOP术语学习

学习aop,免不了学习各种新鲜的术语,结合我们之前的小案例,应该会容易理解的多。

在这里插入图片描述

  • 切面(Aspect):也就是我们定义的专注于提供辅助功能的模块,比如安全管理,日志信息等。

  • 连接点(JoinPoint):切面代码可以通过连接点切入到正常业务之中,图中每个方法的每个点都是连接点。

  • 切入点(PointCut):一个切面不需要通知所有的连接点,而在连接点的基础之上增加切入的规则,选择需要增强的点,最终真正通知的点就是切入点。

  • 通知方法(Advice):就是切面需要执行的工作,主要有五种通知:

    • 前置通知Before:目标方法调用之前执行的通知。
    • 后置通知After:目标方法完成之后,无论如何都会执行的通知。
    • 返回通知AfterReturning:目标方法成功之后调用的通知。
    • 异常通知AfterThrowing:目标方法抛出异常之后调用的通知。
    • 环绕通知Around:可以看作前面四种通知的综合。
  • 织入(Weaving):将切面应用到目标对象并创建代理对象的过程,SpringAOP选择再目标对象的运行期动态创建代理对象。

四、切入点表达式

上面提到:连接点增加切入规则就相当于定义了切入点,当然切入点表达式分为两种:within和execution,这里主要学习execution表达式。

  • 写法:execution(访问修饰符 返回值 包名.包名……类名.方法名(参数列表))

  • 例:execution(public void com.smday.service.impl.AccountServiceImpl.saveAccount())

  • 访问修饰符可以省略,返回值可以使用通配符*匹配。

  • 包名也可以使用*匹配,数量代表包的层级,当前包可以使用..标识,例如* *..AccountServiceImpl.saveAccount()

  • 类名和方法名也都可以使用*匹配:* *..*.*()

  • 参数列表使用..可以标识有无参数均可,且参数可为任意类型。

全通配写法:* .*(…)

通常情况下,切入点应当设置再业务层实现类下的所有方法:* com.smday.service.impl.*.*(..)

五、SpringAOP总结

  1. 获取对象时,生成目标对象的代理对象。

  2. 根据切入点规则,匹配用户连接点,得到切入点。

  3. 当切入点被调用时,通过代理对象拦截。

  4. 由切面类中的指定的通知执行来进行增强。

Spring自动为目标对象生成代理对象,默认情况下,如果目标对象实现过接口,则采用java的动态代理机制,如果目标对象没有实现过接口,则采用cglib动态代理。

六、简单小实例

一、异常信息写入文件

@Component
@Aspect
public class ExceptionAspect {
    private FileWriter writer = null;
    {
        try{
            writer = new FileWriter("err.log");
        }catch (IOException e){
            e.printStackTrace();
        }
    }
    @AfterThrowing(value = "execution(* com.smday.service.impl.*.*(..))",throwing = "t")
    public void afterThrowing(JoinPoint joinPoint,Throwable t)throws Exception{
        //获取类型信息
        Class<?> aClass = joinPoint.getTarget().getClass();
        //获取方法名
        String name = joinPoint.getSignature().getName();
        //获取异常信息
        String msg = t.getMessage();
        String err = "["+aClass+"] == ["+name+"] == ["+msg+"]";
        writer.write(err);
        writer.flush();
    }
}

二、权限简单管理

  1. 自定义注解
/**
 * 自定义权限注解
 * @author Summerday
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Authority {
    String value();
}
  1. 定义切面
@Component
@Aspect
public class AuthorityAspect {
    @Around("execution(* com.smday.service.impl.*.*(..))&&@annotation(authority)")
    public Object around(ProceedingJoinPoint pjp, Authority authority) throws Throwable {
        //获取方法注解定义权限
        String value = authority.value();
        //方法名
        String name = pjp.getSignature().getName();
        //权限列表
        List<String> authorityList = AopTest.getAuthorityList();
        System.out.println("当前用户拥有的权限列表为:"+ authorityList);
        Object proceed = null;
        if(authorityList.contains(value)){
            System.out.println("==> ["+name+"]方法已拥有权限...");
             proceed = pjp.proceed();
        }else {
            System.out.println("==> ["+name+"]方法并没有权限...");
        }
        return proceed;
    }
}
  1. 测试
public class AopTest {
    private static final ThreadLocal<List<String>> AuthorityList = new ThreadLocal<List<String>>();
    static {
        List<String> list = new ArrayList<String>();
        list.add("delete");
        AuthorityList.set(list);
    }
    public static List<String> getAuthorityList() {
        return AuthorityList.get();
    }
    public static void main(String[] args) {
        //获取容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //获取对象
        final AccountService as = ac.getBean(AccountService.class);
        as.deleteAccount();
        System.out.println("=====================================");
        as.saveAccount();
    }
}

在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章