Spring AOP介绍与使用

介绍

面向切面编程(Aspect-oriented Programming 简称AOPAOP) ,是相对面向对象编程(Object-oriented Programming 简称OOP)的框架,作为OOP的一种功能补充. OOP主要的模块单元是类(class)。而AOP则是切面(aspect)。切面会将诸如事务管理这样跨越多个类型和对象的关注点模块化(在AOP的语义中,这类关注点被称为横切关注点(crosscutting))。
AOP是Spring框架重要的组件,虽然Spring IoC容器没有依赖AOP,因此Spring不会强迫开发者使用AOP。但AOP提供了非常棒的功能,用做对Spring IoC的补充。
Spring 2.0+ AOP
Spring 2.0 引入了一种更简单、更强大的方式用来自定义切面,开发者可以选择使用基于模式 schema-based approach 的方式或使用@AspectJ注解风格方式来定义。这两种方式都完全支持通知(Advice)类型和AspectJ的切点语义,虽然实际上仍然是使用Spring AOP织入(weaving)的。
AOP在Spring Framework中用于:
  • 提供声明式企业服务,特别是用于替代EJB的声明式服务。最重要的服务是声明式事务管理(declarative transaction management),这个服务建立在Spring的抽象事务管理(transaction abstraction)之上。
  • 允许开发者实现自定义切面,使用AOP来完善OOP的功能。
如果只打算使用通用的声明式服务或者已有的声明式中间件服务,例如缓冲池(pooling)那么可以不直接使用AOP,也可以忽略本章大部分内容。

 为什么要引入AOP?

 
@Service
public class RoleServiceImpl  implements RoleService { 

    public Role get(Integer id) {
        System.out.println("查询Role");
        return new Role();
    }

    public void add(Role role) {
        System.out.println("添加Role");
    }

    public void delete(Integer id) {
        // 日志
        System.out.println("删除Role");

    }

    public void update(Role role) {
        System.out.println("修改Role");
    }
}

此代码非常简单,就是基础的三层CRUD的代码实现,此时如果需要添加日志功能应该怎么做呢,只能在每个方法中添加日志输出,同时如果需要修改的话会变得非常麻烦。

按照上述方式抽象之后,代码确实简单很多,但是大家应该已经发现在输出的信息中并不包含具体的方法名称,我们更多的是想要在程序运行过程中动态的获取方法的名称及参数、结果等相关信息,此时可以通过使用代理的方式来进行实现。

AOP的底层用的代理,代理是一种设计模式

静态代理

弊端:需要为每一个被代理的类创建一个“代理类”,虽然这种方式可以实现,但是成本太高

动态代理(AOP的底层是用的动态)

jdk动态代理 :必须保证被代理的类实现了接口,

cglib动态代理 :不需要接口

package com.example.inter;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;

/**
 * 帮助Calculator生成代理对象的类
 */
public class CalculatorProxy {

    /**
     *
     *  为传入的参数对象创建一个动态代理对象
     * @param calculator 被代理对象
     * @return
     */
    public static Calculator getProxy(final Calculator calculator){


        //被代理对象的类加载器
        ClassLoader loader = calculator.getClass().getClassLoader();
        //被代理对象的接口
        Class[] interfaces = calculator.getClass().getInterfaces();
        //方法执行器,执行被代理对象的目标方法
        InvocationHandler h = new InvocationHandler() {
            /**
             *  执行目标方法
             * @param proxy 代理对象,给jdk使用,任何时候都不要操作此对象
             * @param method 当前将要执行的目标对象的方法
             * @param args 这个方法调用时外界传入的参数值
             * @return
             * @throws Throwable
             */
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                //利用反射执行目标方法,目标方法执行后的返回值
//                System.out.println("这是动态代理执行的方法");
                Object result = null;
                try {
                    System.out.println(method.getName()+"方法开始执行,参数是:"+ Arrays.asList(args));
                    result = method.invoke(calculator, args);
                    System.out.println(method.getName()+"方法执行完成,结果是:"+ result);
                } catch (Exception e) {
                    System.out.println(method.getName()+"方法出现异常:"+ e.getMessage());
                } finally {
                    System.out.println(method.getName()+"方法执行结束了......");
                }
                //将结果返回回去
                return result;
            }
        };
        Object proxy = Proxy.newProxyInstance(loader, interfaces, h);
        return (Calculator) proxy;
    }
}·

我们可以看到这种方式更加灵活,而且不需要在业务方法中添加额外的代码,这才是常用的方式。如果想追求完美的,还可以使用下述的日志工具类来完善。

package com.example.util;

import java.lang.reflect.Method;
import java.util.Arrays;

public class LogUtil {

    public static void start(Method method, Object ... objects){
        System.out.println(method.getName()+"方法开始执行,参数是:"+ Arrays.asList(objects));
    }

    public static void stop(Method method,Object ... objects){
        System.out.println(method.getName()+"方法开始执行,参数是:"+ Arrays.asList(objects));

    }

    public static void logException(Method method,Exception e){
        System.out.println(method.getName()+"方法出现异常:"+ e.getMessage());
    }

    public static void end(Method method){
        System.out.println(method.getName()+"方法执行结束了......");
    }
}
package com.example.inter;

import com.example.util.LogUtil;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * 帮助Calculator生成代理对象的类
 */
public class CalculatorProxy {

    /**
     *
     *  为传入的参数对象创建一个动态代理对象
     * @param calculator 被代理对象
     * @return
     */
    public static Calculator getProxy(final Calculator calculator){


        //被代理对象的类加载器
        ClassLoader loader = calculator.getClass().getClassLoader();
        //被代理对象的接口
        Class[] interfaces = calculator.getClass().getInterfaces();
        //方法执行器,执行被代理对象的目标方法
        InvocationHandler h = new InvocationHandler() {
            /**
             *  执行目标方法
             * @param proxy 代理对象,给jdk使用,任何时候都不要操作此对象
             * @param method 当前将要执行的目标对象的方法
             * @param args 这个方法调用时外界传入的参数值
             * @return
             * @throws Throwable
             */
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                //利用反射执行目标方法,目标方法执行后的返回值
//                System.out.println("这是动态代理执行的方法");
                Object result = null;
                try {
                    LogUtil.start(method,args);
                    result = method.invoke(calculator, args);
                    LogUtil.stop(method,args);
                } catch (Exception e) {
                    LogUtil.logException(method,e);
                } finally {
                    LogUtil.end(method);
                }
                //将结果返回回去
                return result;
            }
        };
        Object proxy = Proxy.newProxyInstance(loader, interfaces, h);
        return (Calculator) proxy;
    }
}

看到上述代码之后可能感觉已经非常完美了,但是要说明的是,这种动态代理的实现方式调用的是jdk的基本实现,如果需要代理的目标对象没有实现任何接口,那么是无法为他创建代理对象的,这也是致命的缺陷。而在Spring中我们可以不编写上述如此复杂的代码,只需要利用AOP,就能够轻轻松松实现上述功能,当然,Spring AOP的底层实现也依赖的是动态代理。

AOP的核心概念及术语

  • 切面(Aspect): 指关注点模块化,这个关注点可能会横切多个对象。事务管理是企业级Java应用中有关横切关注点的例子。 在Spring AOP中,切面可以使用通用类基于模式的方式(schema-based approach)或者在普通类中以@Aspect注解(@AspectJ 注解方式)来实现。
  • 连接点(Join point): 在程序执行过程中某个特定的点,例如某个方法调用的时间点或者处理异常的时间点。在Spring AOP中,一个连接点总是代表一个方法的执行。
  • 通知(Advice): 在切面的某个特定的连接点上执行的动作。通知有多种类型,包括“around”, “before” and “after”等等。通知的类型将在后面的章节进行讨论。 许多AOP框架,包括Spring在内,都是以拦截器做通知模型的,并维护着一个以连接点为中心的拦截器链。
  • 切点(Pointcut): 匹配连接点的断言。通知和切点表达式相关联,并在满足这个切点的连接点上运行(例如,当执行某个特定名称的方法时)。切点表达式如何和连接点匹配是AOP的核心:Spring默认使用AspectJ切点语义。
  • 引入(Introduction): 声明额外的方法或者某个类型的字段。Spring允许引入新的接口(以及一个对应的实现)到任何被通知的对象上。例如,可以使用引入来使bean实现 IsModified接口, 以便简化缓存机制(在AspectJ社区,引入也被称为内部类型声明(inter))。
  • 目标对象(Target object): 被一个或者多个切面所通知的对象。也被称作被通知(advised)对象。既然Spring AOP是通过运行时代理实现的,那么这个对象永远是一个被代理(proxied)的对象。
  • AOP代理(AOP proxy):AOP框架创建的对象,用来实现切面契约(aspect contract)(包括通知方法执行等功能)。在Spring中,AOP代理可以是JDK动态代理或CGLIB代理。
  • 织入(Weaving): 把切面连接到其它的应用程序类型或者对象上,并创建一个被被通知的对象的过程。这个过程可以在编译时(例如使用AspectJ编译器)、类加载时或运行时中完成。 Spring和其他纯Java AOP框架一样,是在运行时完成织入的。

AOP的通知类型

  • 前置通知(Before advice): 在连接点之前运行但无法阻止执行流程进入连接点的通知(除非它引发异常)。
  • 后置返回通知(After returning advice):在连接点正常完成后执行的通知(例如,当方法没有抛出任何异常并正常返回时)。
  • 后置异常通知(After throwing advice): 在方法抛出异常退出时执行的通知。
  • 后置通知(总会执行)(After (finally) advice): 当连接点退出的时候执行的通知(无论是正常返回还是异常退出)。
  • 环绕通知(Around Advice):环绕连接点的通知,例如方法调用。这是最强大的一种通知类型。环绕通知可以在方法调用前后完成自定义的行为。它可以选择是否继续执行连接点或直接返回自定义的返回值又或抛出异常将执行结束。
环绕通知是最常用的一种通知类型。与AspectJ一样,在选择Spring提供的通知类型时,团队推荐开发者尽量使用简单的通知类型来实现需要的功能。例如, 如果只是需要使用方法的返回值来作缓存更新,虽然使用环绕通知也能完成同样的事情,但是仍然推荐使用后置返回通知来代替。使用最合适的通知类型可以让编程模型变得简单, 还能避免很多潜在的错误。例如,开发者无需调用于环绕通知(用JoinPoint)的 proceed()方法,也就不会产生调用的问题。
在Spring 2.0中,所有通知参数都是静态类型的,因此您可以使用相应类型的通知参数(例如,方法执行的返回值的类型)而不是Object数组。
切点和连接点匹配是AOP的关键概念,这个概念让AOP不同于其它仅仅提供拦截功能的旧技术。切入点使得通知可以独立于面向对象的层次结构进行定向。 例如,您可以将一个提供声明式事务管理的通知应用于跨多个对象(例如服务层中的所有业务操作)的一组方法。

AOP的应用场景

  • 日志管理
  • 权限认证
  • 安全检查
  • 事务控制

Spring AOP的简单配置

通过动态代理的方式实现日志功能的,但是比较麻烦,现在我们将要使用spring aop的功能实现此需求,其实通俗点说的话,就是把LogUtil的工具类换成另外一种实现方式。

1、在ioc的基础上添加pom依赖


    org.aspectj
    aspectjweaver
    1.9.5



    org.springframework
    spring-aspects
    5.2.3.RELEASE

 2、编写配置

将目标类和切面类加入到IOC容器中,在对应的类上添加组件注解

        给LogUtil添加@Component注解

        给MyCalculator添加@Service注解

        添加自动扫描的配置


        设置程序中的切面类

                在LogUtil.java中添加@Aspect注解

        设置切面类中的方法是什么时候在哪里执行

                在增强模块的类上面标记

                        声明为切面

                        将切面交给spring去管理

@Aspect
@Component

package com.example.util;

import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class LogUtil {

    /*
    设置下面方法在什么时候运行
        @Before:在目标方法之前运行:前置通知
        @After:在目标方法之后运行:后置通知
        @AfterReturning:在目标方法正常返回之后:返回通知
        @AfterThrowing:在目标方法抛出异常后开始运行:异常通知
        @Around:环绕:环绕通知

        当编写完注解之后还需要设置在哪些方法上执行,使用表达式
        execution(访问修饰符  返回值类型 方法全称)
     */
    // 前置通知
    @Before("execution(* com.example.service..*.*(..))")
    public static void before(){
           /* System.out.println(method.getName()+"方法运行前,参数是"+
                    (args==null?"": Arrays.asList(args).toString()));*/

        System.out.println("方法前");
    }

    // 后置通知
    @After("execution(* com.example.service..*.*(..))")
    public static void after(){
           /* System.out.println(method.getName() +"方法运行后,参数是"+
                    (args==null?"": Arrays.asList(args).toString()));*/
        System.out.println("方法后");
    }

    // 后置异常通知
    @AfterThrowing("execution(* com.example.service..*.*(..))")
    public static void afterException(){
        // System.out.println("方法报错了:"+ex.getMessage());

        System.out.println("方法异常");
    }

    // 后置返回通知
    @AfterReturning("execution(* com.example..*.*(..))")
    public static void afterEnd(){
        //System.out.println("方法结束,返回值是:"+returnValue);
        System.out.println("方法返回");

    }
}

开启基于注解的aop的功能



    
    

3、测试

import com.example.inter.Calculator;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MyTest {
  public static void main(String[] args){
      ApplicationContext context = new ClassPathXmlApplicationContext("aop.xml");
      Calculator bean = context.getBean(Calculator.class);
      bean.add(1,1);
  }
}

spring AOP的动态代理方式是jdk自带的方式,容器中保存的组件是代理对象com.sun.proxy.$Proxy对象

4、通过cglib来创建代理对象

package com.example.inter;

import org.springframework.stereotype.Service;

@Service
public class MyCalculator {
    public int add(int i, int j) {
        int result = i + j;
        return result;
    }

    public int sub(int i, int j) {
        int result = i - j;
        return result;
    }

    public int mult(int i, int j) {
        int result = i * j;
        return result;
    }

    public int div(int i, int j) {
        int result = i / j;
        return result;
    }
}
public class MyTest {
    public static void main(String[] args){
        ApplicationContext context = new ClassPathXmlApplicationContext("aop.xml");
        MyCalculator bean = context.getBean(MyCalculator.class);
        bean.add(1,1);
        System.out.println(bean);
        System.out.println(bean.getClass());
    }
}

可以通过cglib的方式来创建代理对象,此时不需要实现任何接口,代理对象是

class com.example.inter.MyCalculator$$EnhancerBySpringCGLIB$$1f93b605类型

综上所述:在spring容器中,如果有接口,那么会使用jdk自带的动态代理,如果没有接口,那么会使用cglib的动态代理。

你可能感兴趣的:(#,Spring,spring,java,开发语言,后端)