手把手教你spring aop的使用,一下子明明白白的(其一)

323 阅读16分钟

一、Spring AOP的介绍

AOP:Aspect Oriented Programming 面向切面编程

OOP:Object Oriented Programming 面向对象编程

面向切面编程:基于OOP基础之上新的编程思想,OOP面向的主要对象是类,而AOP面向的主要对象是切面,在处理日志、安全管理、事务管理等方面有非常重要的作用。AOP是Spring中重要的核心点,虽然IOC容器没有依赖AOP,但是AOP提供了非常强大的功能,用来对IOC做补充。通俗点说的话就是在程序运行期间。

在不修改原有代码的情况下强跟主要业务没有关系的公共功能代码之前写好的方法中的指定位置这种编程的方式叫AOP

二、AOP的概念

为什么要引入AOP?

假如我们已经有了一个写好的项目,现在想给项目的核心功能点添加一些日志功能,那按照以往编码,我们就需要在每个核心功能点进行打印日志的操作。那假如这些方法N多,就需要每个方法都修改,那岂不是要怀疑人生了

这样就需要AOP来帮我们实现了,AOP的底层用的代理,代理是一种设计模式,代理主要分为两种模式,如下:

静态代理
弊端:需要为每一个被代理的类创建一个“代理类”,虽然这种方式可以实现,但是成本太高
动态代理(AOP的底层是用的动态)
jdk动态代理 :必须保证被代理的类实现了接口
cglib动态代理 :不需要接口

这个地方说道代理,那代理是什么意思呢?比如我们生活中需要购买火车票,机票等,可能去官网买不到,这时候就去一些代购网站,让他们帮我们抢票,他们就是充当我们的代理。
我们举个例子,比如张三特别爱玩游戏,技术特别不好,但是呢他还想快速升级,因此他就找到一个游戏代理帮他玩。我们来编写代码

三、静态代理

3-1、游戏玩家自己玩

3-1-1、创建一个游戏核心接口

package com.jony.proxy.statically;

public interface IGamePlayer {
    //登录游戏
    public void login();
    //开始玩游戏
    public void play();
}

3-1-2、创建我们的游戏玩家并实现玩游戏接口

package com.jony.proxy.statically;

public class GamePlayer implements IGamePlayer{
    private String name;

    public GamePlayer(String name) {
        this.name = name;
    }

    @Override
    public void login() {
        System.out.println(name+"-登录游戏");
    }

    @Override
    public void play() {
        System.out.println(name+"-开始玩游戏,被击杀");
    }
}

3-1-3、测试

image.png

3-2、让代理用玩家账号密码玩游戏

3-2-1、代理类

package com.jony.proxy.statically;

public class GameProxyPlayer implements IGamePlayer{
    private String name;
    private GamePlayer gamePlayer;

    public GameProxyPlayer(String name){
        this.name=name;
        this.gamePlayer=new GamePlayer(name);
    }

    @Override
    public void login() {
        System.out.println("拿到"+name+"用户名及密码");
        gamePlayer.login();
    }

    @Override
    public void play() {
        System.out.println("代理击杀其他玩家,赢得了游戏");
    }
}

3-2-2、测试

可以看到已经是代理用玩家的账号密码开始玩游戏了 image.png

总结

用代理,可以在不修改原来类的基础之上,就可以完成实现代理模式。但是假如现在,张三又想购买外卖订餐,这个时候我们又得创建一个新的外卖代理类,这样静态代理就比较繁琐了。

四、动态代理

通过上面的示例,我们发现静态代理的一些弊端,那下面我们来看一下动态代理,假如我们设置一个计算器功能,然后每次运算的时候都需要打印日志。

4-1、创建计算器类

如下计算器类,现在给第一个方法加了一些日志输出,如果我们还想给项目的方法也加上输出,如果没有动态代理,那么就需要挨个方法进行修改,太繁琐了。

package com.jony.proxy.dynamic;

public class Calculator {

    public Integer add(Integer i,Integer j){
        System.out.println("参数为:"+i+",另一个参数是:"+j);
        Integer result=i+j;
        System.out.println("计算结果为:"+result);
        return result;
    }

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

    public Integer mul(Integer i,Integer j){
        Integer result=i*j;
        return result;
    }

    public Integer div(Integer i,Integer j){
        Integer result=i/j;
        return result;
    }
}

4-2、使用动态搭理

首先创建一个动态代理的类

package com.jony.proxy.dynamic;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class MyInvocationHandler implements InvocationHandler {
    Object proxyClass;

    public MyInvocationHandler(Object proxyClass) {
        this.proxyClass = proxyClass;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object resutlt=method.invoke(proxyClass,args);
        return resutlt;
    }
}

方法中,定义了一个Object类型的被代理的类,这样就可以代理所有的类,然后创建构造函数,接收外部传入的类。

Object resutlt=method.invoke(proxyClass,args);

这行代码实际上就是执行被代理类的方法,invoke中传入被代理的类,以及参数。\

测试

动态搭理实际上是基于反射机制实现的。动态代理类如下:

package com.jony.proxy.dynamic;

import org.junit.Test;

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

public class Maintest {

    @Test
    public void test(){

        ClassLoader classLoader = ICalcator.class.getClassLoader();

        Class<?>[] interfaces=new Class[]{ICalcator.class};


        MyInvocationHandler invocationHandler=new MyInvocationHandler(new Calculator());
        /*
        newProxyInstance需要传入以下参数,各参数含义
        ClassLoader loader :类加载器,通常指定的被代理类的类加载器
        Class<?>[] interfaces,:类型,通常指被代理类的接口类型
        InvocationHandler h :委托执行的处理类,比如日志功能
        */
        ICalcator o= (ICalcator) Proxy.newProxyInstance(classLoader,interfaces,invocationHandler);
        System.out.println(o.getClass());
        //调用方法
        System.out.println(o.add(1,1));
    }
}

创建类对象,我们主要使用如下代码

ICalcator o= (ICalcator) Proxy.newProxyInstance(classLoader,interfaces,invocationHandler);

Proxy是JDK为我们提供的代理类,然后newProxyInstance需要传递的三个参数所代表的含义在上面也进行注释。然后这三个参数的创建按照上面的方式写即可。

4-3、运行代码

image.png 代码可以看到,类成功输出,计算的add方法也被成功执行。 注意:所有被代理的方法都会经过InvocationHandler中的invoke方法

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    Object resutlt=method.invoke(proxyClass,args);
    return resutlt;
}

4-3-1、invoke方法的使用

在调用invoke的时候实际上还没开始调用我们类里面实际的方法,我们的日志就可以在这个地方添加

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    System.out.println("调用了"+proxyClass+"的方法"+method.getName()+",参数为:"+ Arrays.toString(args));
    Object resutlt=method.invoke(proxyClass,args);
    System.out.println("返回结果为:"+resutlt);
    return resutlt;
}

这样我们就可以获得调用的类,方法及参数了。

然后我们再调用mul方法如下:

image.png

这样通过动态代理类,我们就不用给每个被代理类创建代理类了。

4-4、使用动态代理类,测试我们刚刚代理玩游戏

4-4-1、首先我们先改造一下创建代理的方法

public static Object CreateProxy(Object objClass){
    //获得被代理类的类加载器
    ClassLoader classLoader = objClass.getClass().getClassLoader();
    //获得被代理类的接口类型
    Class<?>[] interfaces=objClass.getClass().getInterfaces();
    //委托执行处理类
    MyInvocationHandler invocationHandler=new MyInvocationHandler(objClass);
    /*
    newProxyInstance需要传入以下参数,各参数含义
    ClassLoader loader :类加载器,通常指定的被代理类的类加载器
    Class<?>[] interfaces,:类型,通常指被代理类的接口类型
    InvocationHandler h :委托执行的处理类,比如日志功能
    */
    Object o= Proxy.newProxyInstance(classLoader,interfaces,invocationHandler);
    return o;
}

通过以上改造,我们只需要传入被代理类就可以完成代理了

4-4-2、测试

public void test01(){
    GamePlayer gamePlayer=new GamePlayer("张三");
    IGamePlayer iGamePlayer= (IGamePlayer) CreateProxy(gamePlayer);
    iGamePlayer.login();
    iGamePlayer.play();
}

image.png 可以看到我们创建的代理已经成功代理了GamePlayer。

需要注意的是:我们在接收的时候使用的是一个接口类型,如:IGamePlayer.

通过动态代理,这样就可以解决我们在一开始说的加日志功能了,但是这样也有一些弊端,如下:

弊端
1、我们所有的对象已经用spring的IOC管理了,这样使用就不符合IOC规则了。
2、JDK动态代理必须声明一个接口才行

综上:AOP就帮我们解决了以上问题
1、如果有接口就使用JDK动态代理,如果没有接口就使用cglib动态代理。
2、创建完还会把对象放到IOC容器中。而且只需要注解就可以实现了,对被代理的方法更灵活
3、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框架一样,是在运行时完成织入的。

比如我们要实现一个LogUtils的日志切面,然后LogUtils类中有四个方法可以接收通知。在Calculater中有四个方法,然后LogUtils中的四个通知就可以对这四个方法进行切面通知管理,当然AOP也可以自由的对某个方法,进行不同的通知。

Spring AOP 核心概念.jpg

5-1、AOP的通知类型

前置通知(Before advice): 在连接点之前运行但无法阻止执行流程进入连接点的通知(除非它引发异常)。

后置返回通知(After returning advice):在连接点正常完成后执行的通知(例如,当方法没有抛出任何异常并正常返回时)。

后置异常通知(After throwing advice): 在方法抛出异常退出时执行的通知。

后置通知(总会执行)(After (finally) advice): 当连接点退出的时候执行的通知(无论是正常返回还是异常退出)。

环绕通知(Around Advice):环绕连接点的通知,例如方法调用。这是最强大的一种通知类型,。环绕通知可以在方法调用前后完成自定义的行为。它可以选择是否继续执行连接点或直接返回自定义的返回值又或抛出异常将执行结束。

5-2、spring aop的简单配置

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

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

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.5</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.2.6.RELEASE</version>
</dependency>

5-2-2、编写配置

5-2-2-1、先准备我们的切面程序

在LogUtils中,分别加入了四个通知

package com.jony.proxy.aspect;

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

public class LogUtils{

    //前置通知
    public static void before(Method method,Object[] args){
        System.out.println(method.getName()+"方法运行前,参数是:"+(args==null?"": Arrays.toString(args)));
    }

    //后置通知
    public static void after(Method method,Object[] args){
        System.out.println(method.getName()+"方法运行后,参数是:"+(args==null?"": Arrays.toString(args)));
    }

    //后置异常通知
    public static void afterException(Method method,Object[] args){
        System.out.println(method.getName()+"方法运行后报错,参数是:"+(args==null?"": Arrays.toString(args)));
    }

    //后置返回通知
    public static void afterEnd(Method method,Object[] args){
        System.out.println(method.getName()+"方法运行返回,参数是:"+(args==null?"": Arrays.toString(args)));
    }

}

5-2-2-2 添加相关注解

1、给LogUtils类添加@Aspect和@Component
@Aspect//设置切面
@Component //将类注册到IOC容器中,让spring管理,只要这样切面才会生效
2、execution定义切点的不同方式

通过execution可以设置切点的不同方式,语法如下

execution(<修饰符模式>?<返回类型模式><方法名模式>(<参数模式>)<异常模式>?)

参数部分允许使用通配符:
"* " 匹配任意字符,但只能匹配一个元素
".. " 匹配任意字符,可以匹配任意多个元素,表示类时,必须和*联合使用
"+ " 必须跟在类名后面,如Superman+,表示类本身和继承或扩展指定类的所有类

1)、通过方法签名定义切点

1、execution(public * * (..)) 匹配所有目标类的public方法
第一个* 代表返回类型
第二个*代表方法名
而..代表任意入参的方法\

如:下面这个就可以匹配到,而修饰符只要是非public就无法匹配到了

public static void getUser(String uid,String rid){}

2、execution(* * To(..)) 匹配目标类所有以To为后缀的方法
第一个* 代表返回类型
而* To代表任意以To为后缀的方法
而..代表任意入参的方法

如:下面这个就可以匹配到,但是只要方法结尾不是To就无法匹配到了(对访问修饰符及参数无限制)

public static void getUserTo(String uid,String rid){}
2)、通过类定义切点

1、 execution(* com.jony.service.IUserService.*(..))匹配IUserService接口的所有方法(包括实现类中覆写的方法), 第一个 * 代表返回任意类型 ,IUserService.*代表IUserService接口中的所有方法

2、 execution(* com.jony.service.IUserService+(..))匹配IUserService接口及其所有实现类的方法,不但匹配实现类中覆写的方法,也包括实现类中不在接口中定义的方法

3)、通过类包定义切点

在类名模式串中,.* 表示包下的所有类,..* 表示包、子孙包下的所有类

1、execution(* com.jony.*(..))匹配com.jony包下所有类的所有方法

2、execution(* com.jony..(..))匹配com.jony包、子孙包下所有类的所有方法.比如 com.jony.dao ,com.jony.service,com.jony.dao.user包下所有类的所有方法都匹配。 当 ..出现在类名中时,必须后面跟表示子孙包下的所有类。

3、execution(* com..Dao.find(..))匹配包名前缀为com的任何包下类名后缀为Dao的方法,方法名必须以find为前缀, 比如com.jony.UserDao#findUserById()方法都是匹配切点。

4)、通过方法入参定义切点

切点表达式中的方法入参部分比较复杂,可以使用* 和.. 通配符。 其中 *表示任意参数类型的参数, 而..表示任意类型的参数且参数个数不限。

1、execution(* getUser(String,int))匹配getUser(String,int)方法,且getUser方法的第一个入参是String,第二个入参是int。 比如 匹配 UserDao#joke(String ,int)方法。如果方法中的入参类型是java.lang包下的,这可以直接使用类名,否则必须使用全限定类名,比如 joke(java.util.List,int)

2、execution(* getUser(String,*))匹配目标类中的getUser()方法,该方法第一个入参为String,第二个入参为任意类型。 比如 getUser(String s1, String s2)和getUser(String s1,double d)都匹配,但是 getUser(String s1, String s2,double d3)不匹配

3、execution(* getUser(String,..))匹配目标类中的getUser方法,该方法的第一个入参为String,后面可以有任意个入参且入参类型不限。 比如 getUser(String s1),getUser(String s1,String s2)和getUser(String s1,double d2,String s3)都匹配。

4、execution(* getUser(Object+))匹配目标类中的getUser()方法,方法拥有一个入参,且入参是Object类型或该类的子类。 它匹配getUser(String s1) 和getUser(Client c) . 如果定义的切点是execution(* getUser(Object)) ,则只匹配getUser(Object object)而不匹配getUser(String s1) 或者getUser(Client c)

3、给通知添加切点
1)、首先准备我们MVC层代码

image.png

2)、给我们切面扩展类添加切点

分别是指了四个通知 @Before、@After、@AfterThrowing、@AfterReturning,方法内输出的参数先暂时注释掉,后面再说参数和方法。

package com.jony.proxy.aspect;

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


@Aspect//设置切面
@Component //将类注册到IOC容器中,让spring管理,只要这样切面才会生效
public class LogUtils{

    //前置通知
    //设置任意返回值,..service包下所有子分包下的所有的类的所有方法,任意参数
    @Before("execution(* com.jony.proxy.service..*.*(..))")
    public static void before(){
        //System.out.println(method.getName()+"方法运行前,参数是:"+(args==null?"": Arrays.toString(args)));
        System.out.println("方法前");
    }

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

    //后置异常通知
    @AfterThrowing("execution(* com.jony.proxy.service..*.*(..))")
    public static void afterException(){
//        System.out.println(method.getName()+"方法运行后报错,参数是:"+(args==null?"": Arrays.toString(args)));
        System.out.println("方法异常");
    }

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

}
3)、通过xml配置扫描包及开启AOP注解

扫描包,之前我们有提到:
1、通过xml配置<context:component-scan base-package="com.jony">来配置扫描包
2、通过JavaConfig的注解@Configruation和@ComponentScan设置扫描包。

<?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">


    <!--扫描包:扫描类中所有注解,不扫描注解不是生效-->
    <context:component-scan base-package="com.jony" >
    </context:component-scan>

    <!--因为我们使用的是注解方式的AOP,所以要开启注解AOP功能-->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
4)、测试

1、我们先正常的方式进行测试

package com.jony.tests;

import com.jony.proxy.service.UserService;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class AopTest {
    ClassPathXmlApplicationContext ioc;

    //设置初始化加载spring_aop.xml
    @Before
    public void init(){
        ioc=new ClassPathXmlApplicationContext("classpath:/spring_aop.xml");
    }

    @Test
    public void tet01(){
        UserService bean = ioc.getBean(UserService.class);
        bean.get(1);
    }
}

image.png 通过上图可以看到,我们在调用查询的时候,我们切面里面的通知已经成功注入。

2、让代码异常测试

image.png

5-3、总结

1、通过给我们切面增强类添加@Aspect和@Component就可以实现切面,然后在类中添加@Before、@After @AfterThrowing @AfterReturning实现四个通知的切点,然后使用excution添加被扫描类包
2、我们的AOP是比较灵活的,如果我们把LogUtils类的@Aspect和@Component注释掉,AOP的功能就不再进行工作。

image.png 3、代码假如AOP步骤
1)、导入jar包,aspectjweaver和spring_aspects
2)、在spring.xml中添加context:component-scan扫描包,让spring扫描,通过添加注解,让spring帮我们管理起来
3)、使用@Aspect设置切面和@Component放到IOC容器里,让spring管理
4)、然后在切面类类名添加连接点使用@Before、@After、@AfterThrowing、@AfterRuning,然后使用excution表达式去定义扫描的地方。
5)、最后需要在spring.xml里面通过aop:aspectj-autoproxy开启aop注解