Spring--三、Spring的AOP(面向切面编程)

AOP概述

什么是AOP

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

简单的说它就是把我们程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的基础上,对我们的已有方法进行增强

AOP的作用及优势

  • 作用: 在程序运行期间,不修改源码对已有方法进行增强。
  • 优势:
    • 减少重复代码
    • 提高开发效率
    • 维护方便

举例说明

先举个小栗子,即自产自销,在很久以前我们购买电脑都是直接从厂家手里购买,但是当生产厂家逐渐扩大,销售网络也逐渐扩大,生产厂家则无法顾及越来越大的销售业务,这时候代理商就出现了。他们通过赚取差价来帮助生产厂家销售产品,生产厂家就能专心生产,而中间赚取差价和销售这个操作就是对原有生产商功能的加强。

所以我们现在知道,要想实现AOP,就需要来看一看代理是个什么东西。

代理Proxy

为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用,代理也可以对被代理类进行增强。

按照代理的创建时期,代理类可以分为两种:

  • 静态:由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的.class文件就已经存在了。

  • 动态:在程序运行时运用反射机制动态创建而成。

    注意:被代理类必须继承一个接口,或继承一个类(但是需要第三方类库(CGLib,被代理类不能用final修饰)支持来代理)

静态代理

静态代理就是程序员自己手动编写的源代码来对被代理对象进行增强。下面讲一个例子:

  • IUserManager接口是用于规范UserManager类的一个接口。
  • UserManager类是被代理类,他有创建用户和删除用户两个方法。
  • UserManagerProxy类是代理类,他将UserManager类的两个方法进行增强(即在不修改源码的基础上添加了一些功能)。

IUserManager:

public interface IUserManager {
    public abstract void addUser(String name);

    public abstract void deleteUser(String name);
}

UserManager:

public class UserManager implements IUserManager {
    public void addUser(String name) {
        System.out.println("添加了用户:" +  name);
    }

    public void deleteUser(String name) {
        System.out.println("删除了用户:" + name);
    }
}

UserManagerProxy:

public class UserManagerProxy implements IUserManager {

    private UserManager usr;

    public UserManagerProxy(UserManager usr) {
        this.usr = usr;
    }

    public void addUser(String name) {
        try {
            System.out.println("start...addUser");
            this.usr.addUser(name);
            System.out.println("end...addUser");
        }catch (Exception e){
            System.out.println("error...addUser");
        }
    }

    public void deleteUser(String name) {
        try {
            System.out.println("start...deleteUser");
            this.usr.deleteUser(name);
            System.out.println("end...deleteUser");
        }catch (Exception e){
            System.out.println("error..deleteUser");
        }
    }
}

main函数调用:

public static void main(String[] args) {
        IUserManager userManager = new UserManagerProxy(new UserManager());
        userManager.addUser("zzz");
        userManager.deleteUser("zzz");
      	}

输出:

添加了用户:zzz
end...addUser
start...deleteUser
删除了用户:zzz
end...deleteUser

看完源码不难发现,其实也就是继承相同的接口,来实现相同的方法,在新类里面来对原有的方法进行增添并调用原有的方法。

缺点:

  • 代理类和委托类实现了相同的接口,代理类通过委托类实现了相同的方法。这样就出现了大量的代码重复。如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。
  • 代理对象只服务于一种类型的对象,如果要服务多类型的对象。势必要为每一种对象都进行代理,静态代理在程序规模稍大时就无法胜任了。
  • 静态代理类只能为特定的接口(Service)服务。如想要为多个接口服务则需要建立很多个代理类。

动态代理

静态代理一个类只能为一个接口服务,那么必定会产生很多代理类,所以能不能让一个代理类完成全部代理功能呢,这时候就需要动态代理来实现了。

动态代理是利用Java的反射机制在运行时动态代理对象,对于要执行相同扩展操作的类或方法由一个统一的类的invoke方法来实现。动态代理需要用到java.lang.reflect.InvocationHandler接口和java.lang.reflect.Proxy类的newProxyInstance()方法。

InvocationHandler接口的实现类是用于对被代理对象(例如UserManager类)功能进行扩展的类,当被代理对象使用某个方法(例如addUser方法)时,会被InvocationHandler接口的实现类拦截,在InvocationHandler实现类的invoke方法中调用被代理对象的方法,并进行一些代码的扩充。那么Proxy的newProxyInstance方法就是将被代理对象和代理对象绑定起来的方法,将被代理对象的类加载器被代理类实现的接口对象还有InvocationHandler实现类传入该方法中就能进行绑定。

java.lang.reflect.InvocationHandler接口:

public interface InvocationHandler {
    /**
     * 该接口的实现类用于拦截被代理对象
     * @param proxy     被代理的对象
     * @param method    需要调用的方法,
     * 				   Method类是一个方法类,可以接收方法对象
     * @param args      方法调用时所需要的参数
     */
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

java.lang.reflect.Proxy类的newProxyInstance方法:

/**
* 该方法用于指定被代理对象关联到哪个InvocationHandler上
*
* @param loader        一个ClassLoader(类加载器)对象,
*                      定义了由哪个ClassLoader对象来对生成的代理对象进行加载
*
* @param interfaces    一个Interface对象的数组,
*                      表示的是我将要给我需要代理的对象提供一组什么接口,
*                      如果我提供了一组接口给它,
*                      那么这个代理对象就宣称实现了该接口(多态),
*                      这样我就能调用这组接口中的方法了
*
* @param h             一个InvocationHandler对象,
*                      表示的是当我这个动态代理对象在调用方法的时候,
*                      会关联到哪一个InvocationHandler对象上
*/
public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)

这里用到的UserManager类和IUserManager接口和静态代理部分相同:

public interface IUserManager {
    public abstract void addUser(String name);

    public abstract void deleteUser(String name);
}

public class UserManager implements IUserManager {
    public void addUser(String name) {
        System.out.println("添加了用户:" +  name);
    }

    public void deleteUser(String name) {
        System.out.println("删除了用户:" + name);
    }
}

接下来要实现java.lang.reflect.InvocationHandler接口:

/**
 * 动态代理类只能代理接口(不支持抽象类),代理类都需要实现InvocationHandler类,
 * 实现invoke方法。该invoke方法就是调用被代理接口的所有方法时需要调用的,
 * 该invoke方法返回的值是被调用方法的返回值
 */
public class LogHandler implements InvocationHandler {
    //被代理的目标对象
    private Object targetObject;
    /**
     * 绑定关系,也就是关联到哪个接口(与具体的实现类绑定)的哪些方法将被调用时,
     * 执行invoke方法。
     */
    public Object newProxyInstance(Object targetObject){
        this.targetObject = targetObject;
        /**
         * 该方法用于指定类加载器,被代理接口,
         * 及关联的InvocationHandler对象,
         * 返回一个代理对象
         */
        return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(),
                targetObject.getClass().getInterfaces(),this);
    }

    //就是前面的InvocationHandler接口的invoke方法
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object ret = null;

        try {
            System.out.println("start..." + method.getName());
            /**
             * 这里的invoke方法不是InvocationHandler接口中的方法了
             * 他是Method类中的invoke方法
             * @参数一 调用该方法的对象(例如当调用addUser时就是UserManager对象)
             * @参数二 调用方法时的参数列表
             * @return 返回被调用方法的返回值
             */
            ret = method.invoke(targetObject, args);
            System.out.println("end..." + method.getName());
        }catch (Exception e){
            e.printStackTrace();
            System.out.println("error..." + method.getName());
            throw e;
        }
        return ret;
    }
}

main函数调用:

public static void main(String[] args) {
        LogHandler logHandler = new LogHandler();
        /**
         * 此处是创建一个IUserManager接口,
         * 用newProxyInstance方法创建一个代理对象,
         * 用强制类型转换将其转换为IUserManager类型。
         * 
         * 注意:这里要定义接口(IUserManager),
         * 不能定义被代理类(UserManager),
         * 因为这里创建的代理对象是继承自接口(IUserManager)的Object对象
         */
        IUserManager iUserManager =
                (IUserManager)logHandler.newProxyInstance(new UserManager());
        iUserManager.addUser("zzz");
        iUserManager.deleteUser("zzz");
    }

输出:

start...addUser
添加了用户:zzz
end...addUser
start...deleteUser
删除了用户:zzz
end...deleteUser

动态代理的优点:

  • 动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler.invoke)。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。而且动态代理的应用使我们的类职责更加单一,复用性更强。

缺点:

  • 性能低下,应尽量避免在性能敏感系统中使用

AOP(AspectOrientedProgramming):将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码---解耦。

正是因为在所有的类里,核心代码之前的操作和核心代码之后的操作都做的是同样的逻辑,因此我们需要将它们提取出来,单独分析,设计和编码,这就是我们的AOP思想。一句话说,AOP只是在对OOP的基础上进行进一步抽象,使我们的类的职责更加单一。

Spring中的AOP

Spring中AOP的细节

说明

我们学习spring的aop,就是通过配置的方式,实现动态代理。

AOP相关术语

  • Joinpoint(连接点): 所谓连接点是指那些被拦截到的点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点。
  • Pointcut(切入点): 所谓切入点是指我们要对哪些Joinpoint进行拦截的定义。
  • Advice(通知/增强):
    • 所谓通知是指拦截到Joinpoint之后所要做的事情就是通知。
    • 通知的类型:前置通知(事务开始前),后置通知(事务开始后),异常通知(发生异常时),最终通知(在finally里面的),环绕通知(整个invoke方法)。
  • Introduction(引介): 引介是一种特殊的通知在不修改类代码的前提下, Introduction可以在运行期为类动态地添加一些方法或Field。
  • Target(目标对象): 代理的目标对象。
  • Weaving(织入): 是指把增强应用到目标对象来创建新的代理对象的过程。 spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。
  • Proxy(代理): 一个类被AOP织入增强后,就产生一个结果代理类。
  • Aspect(切面): 是切入点和通知(引介)的结合。

学习spring中的AOP要明确的事

  • 开发阶段(我们做的)
    • 编写核心业务代码(开发主线):大部分程序员来做,要求熟悉业务需求。
    • 把公用代码抽取出来,制作成通知。(开发阶段最后再做):AOP编程人员来做。
    • 在配置文件中,声明切入点与通知间的关系,即切面。:AOP编程人员来做。
  • 运行阶段(Spring框架完成的): Spring框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。

基于XML的AOP配置

在学习基于XML的AOP配置之前先来看看我们的接口及其实现类和公共代码部分:

接口

public interface IUserManager {
    void addUser();
    void deleteUser(int i);
    int updateUser();
}

实现类

public class UserManager implements IUserManager {
    public void addUser() {
        System.out.println("添加了账户");
    }
    public void deleteUser(int i) {
        System.out.println("删除了账户" + i);
    }
    public int updateUser() {
        System.out.println("更新了账户");
        return 0;
    }
}

公共代码

public class Logger {
    void beforePringLog(){
        System.out.println("前置通知开始记录事务");
    }
    void afterReturningPrintLog(){
        System.out.println("后置通知开始记录事务");
    }
    void afterThrowingPringLog(){
        System.out.println("异常通知开始记录事务");
    }
    void afterPringLog(){
        System.out.println("最终通知开始记录事务");
    }
    /**
     * 环绕通知
     * 问题:
     *      当我们配置了环绕通知之后,切入点方法没有执行,而通知方法执行了。
     * 分析:
     *      通过对比动态代理中的环绕通知代码,
     *      发现动态代理的环绕通知有明确的切入点方法调用,而我们的代码中没有。
     * 解决:
     *      Spring框架为我们提供了一个接口:ProceedingJoinPoint。
     *      该接口有一个方法proceed(),此方法就相当于明确调用切入点方法。
     *      该接口可以作为环绕通知的方法参数,在程序执行时,
     *      spring框架会为我们提供该接口的实现类供我们使用。
     * spring中的环绕通知:
     *      它是spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。
     */
    Object aroundPrintLog(ProceedingJoinPoint proceedingJoinPoint){
        Object rtValue = null;
        try {
            this.beforePringLog();
            Object args[] = proceedingJoinPoint.getArgs();//得到方法执行所需的参数
            rtValue = proceedingJoinPoint.proceed(args);//明确调用业务层方法(切入点方法)
            this.afterReturningPrintLog();
            return rtValue;
        }catch (Throwable a){
            this.afterThrowingPringLog();
            throw new RuntimeException(a);
        }finally {
            this.afterPringLog();
        }
    }
}

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"
       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">
    <!--上面这部分在spring的某个页面中复制的-->

    <!--配置UserManager类-->
    <bean id="UserManager" class="UserManager"></bean>

    <!--spring中基于XML的AOP配置步骤(此处直接引用黑马教程中的注释,自己写的例子没写什么包)
        1、把通知Bean也交给spring来管理
        2、使用aop:config标签表明开始AOP的配置
        3、使用aop:aspect标签表明配置切面
                id属性:是给切面提供一个唯一标识
                ref属性:是指定通知类bean的Id。
        4、在aop:aspect标签的内部使用对应标签来配置通知的类型
               我们现在示例是让printLog方法在切入点方法执行之前之前:所以是前置通知
               aop:before:表示配置前置通知
                    method属性:用于指定Logger类中哪个方法是前置通知
                    pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强

            切入点表达式的写法:
                关键字:execution(表达式)
                表达式:
                    访问修饰符  返回值  包名.包名.包名...类名.方法名(参数列表)
                标准的表达式写法:
                    public void com.itheima.service.impl.AccountServiceImpl.saveAccount()
                访问修饰符可以省略
                    void com.itheima.service.impl.AccountServiceImpl.saveAccount()
                返回值可以使用通配符,表示任意返回值
                    * com.itheima.service.impl.AccountServiceImpl.saveAccount()
                包名可以使用通配符,表示任意包。但是有几级包,就需要写几个*.
                    * *.*.*.*.AccountServiceImpl.saveAccount())
                包名可以使用..表示当前包及其子包
                    * *..AccountServiceImpl.saveAccount()
                类名和方法名都可以使用*来实现通配
                    * *..*.*()
                参数列表:
                    可以直接写数据类型:
                        基本类型直接写名称           int
                        引用类型写包名.类名的方式   java.lang.String
                    可以使用通配符表示任意类型,但是必须有参数
                    可以使用..表示有无参数均可,有参数可以是任意类型
                全通配写法:
                    * *..*.*(..)

                实际开发中切入点表达式的通常写法:
                    切到业务层实现类下的所有方法
                        * com.itheima.service.impl.*.*(..)
    -->

    <!--配置Logger类-->
    <bean id="Logger" class="Logger"></bean>

    <!--配置AOP-->
    <aop:config>
        <!--配置切面 -->
        <aop:aspect id="logAdvice" ref="Logger">
            <!-- 配置通知的类型,并且建立通知方法和切入点方法的关联-->
            <!-- 配置前置通知:在切入点方法执行之前执行-->
            <aop:before method="pringLog" 
                        pointcut="execution(* *..*.*(..))"></aop:before>
            <!-- 配置后置通知:在切入点方法正常执行之后值。它和异常通知永远只能执行一个-->
            <aop:after-returning method="afterReturningPrintLog" 
                                 pointcut="execution(* *..*.*(..))"></aop:after-returning>
            <!-- 配置异常通知:在切入点方法执行产生异常之后执行。它和后置通知永远只能执行一个-->
            <aop:after-throwing method="afterThrowingPringLog" 
                                pointcut="execution(* *..*.*(..))"></aop:after-throwing>
            <!-- 配置最终通知:无论切入点方法是否正常执行它都会在其后面执行-->
            <aop:after method="afterPringLog" 
                       pointcut="execution(* *..*.*(..))"></aop:after>
            
        </aop:aspect>
    </aop:config>

</beans>
通用化切入点表达式
<aop:config>
    <!--配置切面 -->
    <aop:aspect id="logAdvice" ref="Logger">
        <!-- 配置切入点表达式 id属性用于指定表达式的唯一标识。expression属性用于指定表达式内容
              此标签写在aop:aspect标签内部只能当前切面使用。
              它还可以写在aop:aspect外面,此时就变成了所有切面可用
            -->
        <aop:pointcut id="pt" expression="execution(* *..*.*(..))"/>
        <!-- 配置通知的类型,并且建立通知方法和切入点方法的关联-->
        <aop:before method="beforePringLog" pointcut-ref="pt"></aop:before>
        <!-- 配置后置通知:在切入点方法正常执行之后值。它和异常通知永远只能执行一个-->
        <aop:after-returning method="afterReturningPrintLog" 
                             pointcut-ref="pt"></aop:after-returning>
        <!-- 配置异常通知:在切入点方法执行产生异常之后执行。它和后置通知永远只能执行一个-->
        <aop:after-throwing method="afterThrowingPringLog" 
                            pointcut-ref="pt"></aop:after-throwing>
        <!-- 配置最终通知:无论切入点方法是否正常执行它都会在其后面执行-->
        <aop:after method="afterPringLog" pointcut-ref="pt"></aop:after>
    </aop:aspect>
</aop:config>
环绕通知配置
<aop:config>
    <!--配置切面 -->
    <aop:aspect id="logAdvice" ref="Logger">
        <!-- 配置切入点表达式 id属性用于指定表达式的唯一标识。expression属性用于指定表达式内容
              此标签写在aop:aspect标签内部只能当前切面使用。
              它还可以写在aop:aspect外面,此时就变成了所有切面可用
            -->
        <aop:pointcut id="pt" expression="execution(* *..*.*(..))"/>
        <!-- 配置环绕通知 详细的注释请看Logger类中 配置环绕通知后无需再配置其他通知-->
        <aop:around method="aroundPrintLog" pointcut-ref="pt"></aop:around>
    </aop:aspect>
</aop:config>

测试代码

public class test {
    public static void main(String[] args) {
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        IUserManager userManager = (IUserManager) ac.getBean("UserManager");//注意这里要定义接口类型
        userManager.addUser();
        userManager.deleteUser(1);
        System.out.println(userManager.updateUser());
    }
}

基于注解的AOP配置

  1. 配置spring容器中要扫描的包
  2. 在业务层类添加注解@Service
  3. 在通知类添加注解@Component并添加@Aspect注解(表示当前是一个切面类)
  4. 开启Spring对注解AOP的支持(见xml文件)
  5. 配置切入点表达式(见通知类代码)
  6. 在各个通知方法上分别添加注解(见通知类代码)(建议只对环绕通知添加注解,其他通知不添加注解,但是给出的代码中所有通知都加上了注解)

实现类

@Service("UserManager")
public class UserManager implements IUserManager {
    public void addUser() {
        System.out.println("添加了账户");
    }

    public void deleteUser(int i) {
        System.out.println("删除了账户" + i);
    }

    public int updateUser() {
        System.out.println("更新了账户");
        return 0;
    }
}

通知类

@Component("Logger")
@Aspect
public class Logger {
    @Pointcut("execution(* *..*.*(..))")
    private void pt(){}
    //@Before("pt()")
    void beforePringLog(){
        System.out.println("前置通知开始记录事务");
    }
    //@AfterReturning("pt()")
    void afterReturningPrintLog(){
        System.out.println("后置通知开始记录事务");
    }
    //@AfterThrowing("pt()")
    void afterThrowingPringLog(){
        System.out.println("异常通知开始记录事务");
    }
    //@After("pt()")
    void afterPringLog(){
        System.out.println("最终通知开始记录事务");
    }
    @Around("pt()")
    Object aroundPrintLog(ProceedingJoinPoint proceedingJoinPoint){
        Object rtValue = null;
        try {
            this.beforePringLog();
            Object args[] = proceedingJoinPoint.getArgs();
            rtValue = proceedingJoinPoint.proceed(args);
            this.afterReturningPrintLog();
            return rtValue;
        }catch (Throwable a){
            this.afterThrowingPringLog();
            throw new RuntimeException(a);
        }finally {
            this.afterPringLog();
        }
    }
}

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

    <!-- 配置spring创建容器时要扫描的包-->
    <context:component-scan base-package="com.cla"></context:component-scan>

    <!-- 配置spring开启注解AOP的支持 -->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>

不使用XML文件配置

@Configuration 
@ComponentScan(basePackages="com.cla") 
@EnableAspectJAutoProxy 
public class SpringConfiguration { }

接下来请看下一篇。。。

posted @ 2021-02-24 17:13  zhangzeff  阅读(140)  评论(0)    收藏  举报