首页 > 编程语言 > 详细

04-Spring之AOP

时间:2021-03-09 22:25:05      阅读:27      评论:0      收藏:0      [点我收藏+]

一、概述

AOP,面向切面编程Aspect Oriented Programming,面向切面进行程序设计,维护程序的功能模块。

1-1、面向对象编程OOP

OOP(面向对象编程Object Oriented Programming)指的是面向对象将程序抽象成多个层次的对象,每个对象负责不同的功能模块(或者说将一系列相关的属性以及行为封装在一起,形成一个对象),这样子设计的话,每个对象负责不同的模块,分工明确,各司其职。
技术分享图片

1-2、代理模式

在说AOP之前,必须要先了解一下代理模式,代理模式通过代理某个功能/业务,实现对功能/业务的扩展或者隔离

举个例子:现在有一个功能/业务——登录功能/业务(或者将登录改成上课),现在需要登记尝试登录的人的姓名,一般情况下,我们需要修改这个登录功能/业务,在登录之前进行一个登记操作(在登录代码之前,额外增加登记的代码)。这是一种侵入式地修改,侵入地修改原先登录的代码。可以说这种方式增加了代码的耦合性——核心业务和后期增加的业务代码耦合在一起了。

代理模式则是通过代理这个登录功能的代码,实现业务之间的解耦——如果需要添加其他的功能,不需要侵入式地修改原先的核心代码,而只用修改代理者的代码(在Spring中则是通过配置即可实现),同时可以将一个大的核心功能拆分成多个小的功能(功能/业务的隔离)来实现。
技术分享图片

1-3、面向切面编程AOP

AOP(面向切面编程Aspect Oriented Programming)是OOP的延伸,是为了在不修改源代码的前提下,给多个功能/业务添加一些通用的功能,AOP是基于代理模式实现的。

举个例子,如上面OOP中的系统,有着多个功能模块(StudentService、TeacherService、BookService......),如果需要给这几个功能模块都添加一个日志功能时,设计将会变得繁琐——如果编写日志工具类,修改核心业务代码,相当于把两个功能耦合在一起了,如果自己一个一个主动进行代理,也会非常的繁琐,因此有了AOP,不再面向某一个对象,而是面向多个对象的某个切面(过程)进行编程。(这里的多个对象也可以是一个对象,单独给一个对象添加某个功能——其实这就相当于一个直接的代理了,为了演示,这里是面向多个对象的)
技术分享图片

一方面来说,通用功能的扩展更加容易了(不会与核心业务代码进行耦合),从另一方面来说,一些通用功能都被从核心业务中隔离出去了。

下面是不使用AOP的功能扩展:
技术分享图片

二、底层原理

Spring的AOP通过动态代理来实现,其有两种方式,如果类实现了接口将默认使用JDK动态代理,也可以强制选择使用CGLIB动态代理,反之没有实现接口则会使用CGLIB动态代理。

  1. JDK动态代理:对实现了接口的类生成代理对象。如果想要代理某个方法,必须要将其放置到接口中。(JDK动态代理是通过生成一个继承于Proxy同时实现了被代理类接口的一个代理类,因此必须要讲需要代理的方法放置在接口中,否则找不到这个方法,JDK代理性能更好)

    // 被代理类的接口,接口中声明需要进行代理的方法
    public interface IAccountService {
        void saveAccount();
        void delAccount();
        void updateAccount();
        void findAccount();
    }
    // 被代理类,里面是被代理业务的核心代码
    public class AccountServiceImpl implements IAccountService {
        @Override
        public void saveAccount() {
            System.out.println("Service:保存账户");
        }
        @Override
        public void delAccount(){
            System.out.println("Service:删除账户");
        }
        @Override
        public void updateAccount(){
            System.out.println("Service:更新账户");
        }
        @Override
        public void findAccount(){
            System.out.println("Service:查询账户");
        }
    }
    // 其中的invoke将会生成一个代理类,继承于Proxy同时实现了被代理类接口
    class IAccountServiceProxy implements InvocationHandler{
        private Object obj;
    
        public IAccountServiceProxy(Object obj) {
            this.obj = obj;
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Object invoke=null;
            try {
                // 被代理方法执行之前执行的业务,此处可以加入判断给不同的方法执行不同的代码
                System.out.println("代理之前执行的方法"+method.getName());
                // 执行被代理的方法
                invoke = method.invoke(obj, args);
                // 被代理方法执行之后执行的业务
                System.out.println("代理之后执行");
    
            } catch (Exception e) {
                // 被代理方法执行异常时执行的业务
                System.out.println("异常时执行");
            } finally {
                // 被代理方法执行之后,一定会执行的业务
                System.out.println("总会执行");
            }
            return invoke;
        }
    }
    
    //JDK动态代理
    public class JDKProxy {
        public static void main(String[] args) {
            Class[] interfaces={IAccountService.class};
            AccountServiceImpl accountServiceImpl= new AccountServiceImpl();
            IAccountService accountService = (IAccountService) Proxy.newProxyInstance(JDKProxy.class.getClassLoader(), interfaces, new IAccountServiceProxy(accountServiceImpl));
            accountService.findAccount();
            accountService.updateAccount();
        }
    }
    
    
    
  2. CGLIB代理:与JDK动态代理不同,CGLIB代理将会生成被代理类的子类(因此被代理类不需要实现接口,适用范围更广),但是性能稍差。

三、AOP相关术语

AOP的相关术语不直观,这些相关术语不特定于Spring。
Aspect(切面):
	通知和切入点的结合,通知描述了干什么事以及什么时候干(执行方法前、后...),切入点描述了在哪干(执行什么方法时)
Joinpoint(连接点):
	程序执行中的某一点(某一步),如执行的方法或者处理的异常,在SpringAOP中,指的是某个执行的方法。
Advice(通知/增强):
	所谓通知就是指拦截到Joinpoint之后要做的事情就是通知。
	通知的类型:
        前置通知:执行方法前进行的操作--比如进行数据库操作时的连接数据库
        后置通知:执行方法后进行的操作(抛出异常将不会执行)--比如进行数据库操作时操作完数据后提交数据
        异常通知:执行方法出现异常时进行的操作--比如进行数据库操作时的数据提交异常后的事务回滚
        最终通知:一定会执行的操作,最后进行--比如进行数据库操作结束时断开连接
        环绕通知(建议使用):前面四种通知的集合
Pointcut(切入点):
	Jointpoint的一部分,指的是真正被增强的类里面的方法。
Introduction(引介):
	引介是一种特殊的通知,在不修改代码的前提下,Introduction可以在运行期为为类动态地添加一些方法或Field。
Target Object(目标对象):
	代理的目标对象
AOP Proxy(代理):
	一个类被AOP织入增强后,就产生一个结果代理类。
Weaving(织入):
	是指把增强应用到目标对象来创建新的代理对象的过程
	spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。

四、基于注解的AOP配置

4-1、导入依赖

Spring通过使用AspectJ提供的库来解析和匹配切入点中与Aspect相同的注解。

虽然Spring使用@AspectJ注解来提供AOP的服务,但并不是说使用了@AspectJ的注解就表明使用AspectJ这个AOP框架,在Spring中,使用@AspectJ一系列的注解来提供AOP服务,但具体使用Spring AOP还是AspectJ还是需要根据依赖决定的。

<!--maven依赖导入,导入aspcetj库-->
<!--aspectj-->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.0</version>
</dependency>

4-2、案例

4-2-1、开启注解扫描

<!--开启Spring注解扫描-->
<context:component-scan base-package="com.whether"/>

<!--配置spring开启@AspectJ注解扫描,如果不开启,则@AspectJ类的注解将不会被扫描到,也不会生效,如果使用注解来配置AOP,则不需要开启@AspectJ的注解扫描-->
<aop:aspectj-autoproxy/>
注意:里面有一个proxy-target-class=""属性,表示使用JDK或者使用cglib的动态代理,默认为false(JDK),也可以为true(cglib)。

4-2-2、编写通知类

//注意1:环绕通知和其他通知只出现一种即可
//注意2:使用注解时,如果不使用环绕通知,后置通知、异常通知和最终通知的顺序会有问题(先最终通知再后置/异常通知)但是环绕通知不会有这个问题
//这个类的作用就是代理com.whether.service.impl包下的所有类的方法,并配置了前置通知,后置通知,异常通知和最终通知
@Component("logger")
@Aspect               // 表示这是一个切面类
@Order(1)             // 当有多个增强类时,可以通过@Order()指定优先级,数值越小优先级越高,此处只有一个增强类,@Order()没有实际作用
public class Logger {
    // 指明需要代理的切面(功能/业务)
    @Pointcut("execution(* com.whether.service.impl.*.*(..))")
    private void pt1(){}
    /**
     * 前置通知
     * */
    @Before("pt1()")
    public void beforPrintLog(){
        System.out.println("前置通知");
    }
    /**
     * 后置通知
     * */
    @AfterReturning("pt1()")
    public void afterReturningPrintLog(){
        System.out.println("后置通知");
    }
    /**
     * 异常通知
     * */
    @AfterThrowing("pt1()")
    public void afterThrowingPrintLog(){
        System.out.println("异常通知");
    }
    /**
     * 最终通知
     * */
    @After("pt1()")
    public void afterPrintLog(){
        System.out.println("最终通知");
    }
    /**
     * 环绕通知
     * */
    @Around("pt1()")
    public Object aroundPrountLog(ProceedingJoinPoint pjp){
        Object rtValue=null;
        try {
            System.out.println("环绕的前置通知");

            Object[] args=pjp.getArgs();
            rtValue = pjp.proceed(args);//调用业务层方法(切入点方法)

            System.out.println("环绕的后置通知");
            return rtValue;
        } catch (Throwable throwable) {
            System.out.println("环绕的异常通知");
            throwable.printStackTrace();
        }finally {
            System.out.println("环绕的最终通知");
        }
        return rtValue;
    }
}

五、基于xml的AOP配置

4-1、导入依赖

<!--maven依赖导入,导入aspcetj库-->
<!--aspectj-->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.0</version>
</dependency>

4-2、案例

4-2-1、编写通知类

@Component("logger")
public class Logger {
    /**
     * 前置通知
     * */
    public void beforPrintLog(){
        System.out.println("前置通知");
    }
    /**
     * 后置通知
     * */
    public void afterReturningPrintLog(){
        System.out.println("后置通知");
    }
    /**
     * 异常通知
     * */
    public void afterThrowingPrintLog(){
        System.out.println("异常通知");
    }
    /**
     * 最终通知
     * */
    public void afterPrintLog(){
        System.out.println("最终通知");
    }
    /**
     * 环绕通知
     * */
    public Object aroundPrountLog(ProceedingJoinPoint pjp){
        Object rtValue=null;
        try {
            System.out.println("环绕的前置通知");

            Object[] args=pjp.getArgs();
            rtValue = pjp.proceed(args);//调用业务层方法(切入点方法)

            System.out.println("环绕的后置通知");
            return rtValue;
        } catch (Throwable throwable) {
            System.out.println("环绕的异常通知");
            throwable.printStackTrace();
        }finally {
            System.out.println("环绕的最终通知");
        }
        return rtValue;
    }
}

4-2-2、编写xml配置文件

<!--spring中基于xml配置AOP的基本步骤
	简而言之就是指明哪些地方需要进行哪些通知
	aop:aspect.ref指明通知(函数)的所属类
	pointcut:指明哪些地方(函数)需要进行通知
	aop:aspect->aop:before表明要进行前置通知类型(前置通知)
	aop:aspect->aop:before.method,表明需要进行的具体通知
	比如method="printLog",将会执行ref所指类下的printLog()方法
	
	1.把通知bean也交给spring来管理
	2.使用aop:config标签表明开始AOP配置
	3.使用aop:aspect标签表明配置切面,实际上就是指定使用哪个类下的函数进行通知(这个类必须得先交给spring容器管理,在通过ref引用)
		id属性:是给切面提供一个唯一标识
		ref属性:指定通知类的bean的id
	4.在aop:aspect标签的内部使用对应标签来配置通知的类型
		我们现在的实例是让printLogger方法在切入点方法执行之前(前置通知)
		aop:before:表示配置前置通知
			method属性:用于指定Logger类中哪个方法是前置通知
			pointcut属性:用于指定切入点表达式该表达式的含义指的是对业务层中的哪些方法增强
		切入点表达式写法:
			关键字:execution(表达式)
			表达式:
				访问修饰符 返回值 包名.包名...类名.方法名(参数列表)
			标准表达式写法:
				public void service.impl.AccountServiceImpl.saveAccount()
			访问修饰符可以省略:
				void service.impl.AccountServiceImpl.saveAccount()
			返回值可以使用通配符,表示任意返回值:
				* service.impl.AccountServiceImpl.saveAccount()
			包名可以使用通配符,表示任意包,但是有几级包就需要写几个*:
				* *.*.AccountServiceImpl.saveAccount()
			包名可以使用..表示当前包及其子包:
				* *..AccountServiceImpl.saveAccount()
			类名和方法名都可以使用*来实现通配:
				* *..*.saveAccount()
				* *..*.*()---这个是无参的任意方法
			参数列表:
				可以直接写数据类型
					基本类型直接写名称
					引用类型写包名.类名的方式 java.lang.String
				* *..*.saveAccount(int)
				可以使用通配符表示任意类型,但是必须要有参数
					* *..*.saveAccount(*)
				可以使用..表示有无参数均可
			全通配写法:
				* *..*.*(..)
			实际开发中切入点表达式的通常写法:
				切到业务层实现类下的所有方法
					* service.impl.*.*(..)
-->

技术分享图片

<!--pointcut就是指哪些地方需要进行通知-->
<aop:config>
    <!--配置切面,其中ref指定使用什么类的方法进行配置-->
    <aop:aspect id="logAdvice" ref="logger">
        <!--配置前置通知,使用logger类的beforPrintLog方法配置前置通知-->
        <aop:before method="beforPrintLog" pointcut="execution(* com.whether.service.impl.*.*(..))"></aop:before>
        <!--配置后置通知,使用logger类的afterReturningPrintLog方法配置后置通知-->
        <aop:after-returning method="afterReturningPrintLog" pointcut="execution(* com.whether.service.impl.*.*(..))"></aop:after-returning>
        <!--配置异常通知,使用logger类的afterThrowingPrintLog方法配置异常通知-->
        <aop:after-throwing method="afterThrowingPrintLog" pointcut="execution(* com.whether.service.impl.*.*(..))"></aop:after-throwing>
        <!--配置最终通知,使用logger类的afterPrintLog方法配置最终通知-->
        <aop:after method="afterPrintLog" pointcut="execution(* com.whether.service.impl.*.*(..))"></aop:after>
    </aop:aspect>
</aop:config>
相当于
<aop:config>
    <!--配置切面-->
    <aop:aspect id="logAdvice" ref="logger">
        <!--配置切入点表达式 id属性用于指定表达式的唯一标识。expression属性用于指定表达式内容
            此标签写在aop:aspect标签内部时只能当前切面使用
            它还可以写在aop:aspect外面,此时就变成所有切面可用
        -->
        <aop:pointcut id="pt1" expression="execution(* com.whether.service.impl.*.*(..))"/>
        
        <!--配置前置通知-->
        <aop:before method="beforPrintLog" pointcut-ref="pt1"></aop:before>
        <!--配置后置通知-->
        <aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"></aop:after-returning>
        <!--配置异常通知-->
        <aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"></aop:after-throwing>
        <!--配置最终通知-->
        <aop:after method="afterPrintLog" pointcut-ref="pt1"></aop:after>
    </aop:aspect>
</aop:config>
相当于
<aop:config>
    <!--配置切面-->
    <aop:aspect id="logAdvice" ref="logger">
		<!--环绕通知-->
        <aop:around method="aroundPrountLog" pointcut="execution(* com.whether.service.impl.*.*(..))"></aop:around>
    </aop:aspect>
</aop:config>

04-Spring之AOP

原文:https://www.cnblogs.com/whether/p/14508234.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!