Spring AOP

Spring整合单元测试

在前面的案例中我么需要自己创建ApplicationContext对象,然后在调用getBean来获取需要测试的Bean

Spring提供了一种更加方便的方式来创建测试所需的ApplicationContext,并且可以帮助我们把需要测试的Bean直接注入到测试类中

添加依赖:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.2.2.RELEASE</version>
</dependency>

测试代码:

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.annotation.Resource;

@RunWith(SpringJUnit4ClassRunner.class)//固定写法
@ContextConfiguration("classpath:applicationContext.xml") //指定加载的配置文件
public class MyTest2 {
    @Resource(name = "userService")   //直接使用DI获取Bean
    private UserService userService;

    @Test
    public void test(){
        userService.getUserDao().save();//测试
    }
}

AOP概念

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

以上内容来自百度百科,看完你会发现这玩儿说个啥呢?看完和没看差不多,太过于抽象,咱们还是带着问题来看AOP吧;

为什么需要AOP

案例分析

在项目开发中我们经常遇到一系列通用需求比如:权限控制,日志输出,事务管理,数据统计等,这些看似简单的需求,在实际开发中却会带来麻烦,举个例子:

在某个的Dao层如UserDao,存在以下几个方法

public class UserDao{
  public void save(){
    System.out.println("save sql");
  }
  public void delete(){
    System.out.println("delete sql");
  }
  public void update(){
    System.out.println("update sql");
  }
}

在第一个版本中,已经实现了程序的实际功能,但是后来发现数据库操作出现瓶颈,这时领导说要对这些方法进行执行时间统计,并输出日志分析问题;

解决方案

这点小需求对于你来说太easy了,于是你打开了代码,熟练的添加代码

public class UserDao{
    public void save(){
      	//获取类名和方法名
        String className = Thread.currentThread().getStackTrace()[1].getClassName();
        String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
      	//开始时间
        long startTime = new Date().getTime();
      
				//原逻辑
        System.out.println("save sql");
      	
      	//耗时
        long runTime = new Date().getTime() - startTime;
        System.out.printf("info [class:%s method:%s runtime:%s]",className,methodName,runTime);
    }
    public void delete(){
        String className = Thread.currentThread().getStackTrace()[1].getClassName();
        String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
        long startTime = new Date().getTime();

        System.out.println("delete sql");

        long runTime = new Date().getTime() - startTime;
        System.out.printf("info [class:%s method:%s runtime:%s]",className,methodName,runTime);
    }
    public void update(){
	    System.out.println("update sql");
  	}

    public static void main(String[] args) {
        new UserDao().save();
    }
}

问题

需求实现了,但是作为优秀的开发工程师你当然不会就这么完事了,因为上述代码存在以下问题:

  • 1.修改了源代码(违反了OCP,违反了对修改封闭,但满足调用方式不变)

  • 2.大量重复代码

再看AOP

我们先不考虑如何解决这些问题,其实AOP之所以出现就是因为,我们需要对一些已经存在的方法进行功能扩展,但是又不能通过修改源代码或改变调用方式的手段来解决

反过来说就是要在保证不修改源代码以及调用方式不变的情况下为原本的方法增加功能

而由于需要扩展的方法有很多,于是把这些方法称作一个切面,即切面就是一系列需要扩展功能的方法的集合

AOP的目的

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

吐槽:

直接看名字的确是比较抽象的,没办法,当你创造了一个全新的东西时,你往往也会想给它取一个nb的名字,而这个解决方案是针对一些固定场景的,我们很难找到一个非常准确的名字去描述这个方案

假设你想出了一个解决方案,那么你会给他取个什么名字呢?

AOP相关术语

AOP这一概念是AOP联盟aopalliance提出的,相关的概念也出自aopalliance定义

  • 连接点(joinpoint)

是扩展内容与原有内容的交互的点,可以理解为可以被扩展的地方,通常是一个方法,而AspectJ中也支持属性作为连接点

示例:案例中的三个方法

  • 切点(pointcut)

切点指的是要被扩展(增加了功能)的内容,包括方法或属性(joinpoint)

示例:案例中的两个增加了功能的方法

  • 通知(adivce)

通知指的是要在切点上增加的功能

按照执行时机不同分为:

​ 前置,后置,异常,最终,环绕,引介

​ 引介通知指的是在不修改类代码的前提下,为类增加方法或属性(了解即可非重点)

示例:上述案例中的输出执行时间功能

  • 目标(target)

目标就是要应用通知的对象,即要被增强的对象

示例:上述案例中的userDao

  • 织入(weaving)

织入是一个动词,描述的是将扩展功能应用到target的这个过程

示例:案例中修改源代码的过程

  • 代理(proxy)

Spring是使用代理来完成AOP,对某个对象增强后就得到一个代理对象;

Spring AOP的整个过程就是对target应用advice最后产生proxy,我们最后使用的都是proxy对象; 狸猫换太子,偷梁换柱;

  • 切面(aspect)

是切入点和通知的结合切面,是一个抽象概念; 一个切面指的是所有应用了同一个通知的切入点的集合

示例:案例中的save 和 delete方法共同组成一个切面

AOP的传统实现

就在官方努力退出动态代理时,民间开发者也安耐不住自己躁动的新,开发了自己的一套实现AOP的方案,两者都是利用代理对象,都属于代理模式,但是实现原理略有不同;

动态代理(官方)

JDK1.4出现

接口:

public interface UserDao {
    public void save();
    public void delete();
}

实现类:

package com.yh.demo4;

public class UserDaoImpl implements UserDao {
    public void save(){
        System.out.println("save run");
    }
    public void delete(){
        System.out.println("delete run");
    }
}

代理类及测试代码:

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

public class MyProxy implements InvocationHandler {
    private Object target;


    public MyProxy(Object target) {
        this.target = target;
    }

    //创建代理对象 本质是动态的产生一个target对象的接口实现类
    public Object createProxy(){
        Object o = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
        return o;
    }

    //方法处理 动态代理核心方法,在调用代理对象方法时都会自动调用该方法
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //获取类信息和方法名
        String className = target.getClass().getName();
        String methodName = method.getName();
        //记录开始时间
        long startTime = new Date().getTime();

        //调用原始方法
        Object result = method.invoke(target,args);

        //计算耗时
        long runtime = new Date().getTime() - startTime;
        System.out.printf("info [class:%s method:%s runtime:%s]\n",className,methodName,runtime);
        //返回原始方法执行结果
        return result;
    }

    public static void main(String[] args) {
        //目标对象
        UserDao userDao = new UserDaoImpl();
        //代理对象
        UserDao proxyDao = (UserDao) new MyProxy(userDao).createProxy();

        proxyDao.save();
        proxyDao.delete();
    }
}

当我们要对某些方法进行权限控制时也非常简单,只需要判断方法名称,然后增加权限控制逻辑即可;

注意:
1.动态代理,要求被代理的target对象必须实现了某个接口,且仅能代理接口中声明的方法,这给开发带来了一些限制,当target不是某接口实现类时,则无法使用动态代理,CGLib则可以解决这个问题
2.被拦截的方法包括接口中声明的方法以及代理对象和目标对象都有的方法如:toString
3.对代理对象执行这些方法将造成死循环

CGLib(民间)

CGLib是第三方库,需要添加依赖:

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.2.5</version>
</dependency>

被代理类:

public class UserDaoImpl {
    public void save(){
        System.out.println("save run");
    }

    public void delete(){
        System.out.println("delete run");
    }
}

代理类及测试代码:

public class MyProxy implements MethodInterceptor {
    private Object target;
    public MyProxy(Object target) {
        this.target = target;
    }

    //创建代理对象 本质是动态的产生一个target对象的接口实现类
    public Object createProxy(){
        //CGLib核心类
        Enhancer enhancer = new Enhancer();
        //指定要代理的对象类型
        enhancer.setSuperclass(target.getClass());
        //设置方法回调 即代理调用代理对象的方法时会执行的方法
        enhancer.setCallback(this);
        //创建代理对象
        Object o = enhancer.create();
        return o;
    }
    /***
     * @param o 代理对象
     * @param method 客户要执行的方法
     * @param objects 方法参数
     * @param methodProxy 方法代理对象 用于执行父类(目标)方法
     * @return  原始方法的返回值
     * @throws Throwable*/
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        //注意不要对第一个参数(代理对象) 执行任何方法,会造成死循环

        //获取类信息和方法名
        String className = target.getClass().getName();
        String methodName = method.getName();
        //记录开始时间
        long startTime = new Date().getTime();

        //调用原始方法1 传入的是目标对象
        Object result = method.invoke(target,objects);
      
	      //调用原始方法2 传入的是代理对象
        Object result = methodProxy.invokeSuper(o,objects);

        //计算耗时
        long runtime = new Date().getTime() - startTime;
        System.out.printf("info [class:%s method:%s runtime:%s]\n",className,methodName,runtime);
        //返回原始方法执行结果
        return result;
    }

    public static void main(String[] args) {
        //目标对象
        UserDaoImpl userDao = new UserDaoImpl();
        //代理对象
        UserDaoImpl proxyDao = (UserDaoImpl) new MyProxy(userDao).createProxy();

        proxyDao.save();
        proxyDao.delete();
    }
}

注意:
1.CGLib可以拦截代理目标对象的所有方法
2.CGLib采用的是产生一个继承目标类的代理类方式产生代理对象,所以如果类被final修饰将无法使用CGLib

利用上述两种方法我们就可以实现OAP了

Spring中的AOP

Spring在运行期,可以自动生成动态代理对象,不需要特殊的编译器,Spring AOP的底层就是通过JDK动态代理和CGLib动态代理技术 为目标Bean执行横向织入。并且Spring会自动选择代理方式

1.若目标对象实现了若干接口,spring使用JDK的java.lang.reflect.Proxy类代理。

2.若目标对象没有实现任何接口,spring使用CGLIB库生成目标对象的子类。

Spring通知类型

  • 前置org.springframework.aop.MethodBeforeAdvice用于在原始方法执行前的预处理

  • 后置org.springframework.aop.AfterReturningAdvice用于在原始方法执行后的后处理

  • 环绕org.aopalliance.intercept.MethodInterceptor这个名字不知道谁给起的,其实不算是通知,而是叫拦截器,在这里我们可以阻止原始方法的执行,而其他通知做不到

  • 异常org.springframework.aop.ThrowsAdvice用于在原始方法抛出异常时处理

  • 引介org.springframework.aop.IntroductionInterceptor在目标类中添加一些新的方法和属

    性(非重点)

Spring切面类型

普通的切面(Advisor)

普通切面指的是未指定具体切入点的切面,那么将把目标对象中所有方法作为切入点(全部增强)

接口:

public interface StudentDao {
    public void save();
    public void update();
    public void delete();
    public void select();
}

实现类:

public class StudentDaoImpl implements StudentDao {
    public void save() { System.out.println("save run"); }

    public void update() { System.out.println("update run"); }

    public void delete() { System.out.println("delete run"); }

    public void select() { System.out.println("select run"); }
}

配置文件:

<!--目标Bean-->
<bean id="studentDao" class="com.yh.demo7.StudentDaoImpl"/>
<!--通知-->
<bean id="before" class="com.yh.demo7.MyAdvice"/>

<!---代理Bean-->
 <bean id="studentDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
   		 <!--指定目标-->
       <property name="target" ref="studentDao"/>
       <!--指定目标实现的接口-->
       <property name="proxyInterfaces" value="com.yh.demo7.StudentDao"/>
       <!--通知(拦截)Bean的名称 多个用逗号隔开-->
       <property name="interceptorNames" value="before"/>

       <!--其他设置:-->
       <!--告知spring目标对象是否是一个普通类 true时使用CGlib-->
       <property name="proxyTargetClass" value="true"/>
       <!--代理类是否采用单例,默认true-->
       <property name="singleton" value="false"/>
       <!--是否强制使用CGlib-->
       <property name="optimize" value="true"/>
</bean>

通知类:

public class MyAdvice implements MethodBeforeAdvice,AfterReturningAdvice, MethodInterceptor {
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        System.out.println("前置通知....");
    }
    public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
        System.out.println("后置通知....");
    }
  
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        System.out.println("环绕前....");
        Object result = methodInvocation.proceed(); //执行原始方法
        System.out.println("环绕后....");
        return result;
    }
}

测试代码:

@RunWith(SpringJUnit4ClassRunner.class)//固定写法
@ContextConfiguration("classpath:applicationContext4.xml") //指定加载的配置文件
public class MyTest4 {
    @Resource(name = "studentDaoProxy")
    private StudentDao studentDao;
    @Test
    public void test(){
        studentDao.delete();
        studentDao.save();
        studentDao.update();
        studentDao.select();
    }
}

切入点切面使用(PointcutAdvisor)

顾名思义,也就是指定为目标对象中仅某进行增强

  • 使用正则匹配方法的切面:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       https://www.springframework.org/schema/beans/spring-beans.xsd">

        <!--目标Bean-->
        <bean id="studentDao" class="com.yh.demo7.StudentDaoImpl"/>
        <!--通知-->
        <bean id="advice" class="com.yh.demo7.MyAdvice"/>

        <!--组织切面信息-->
        <bean id="myAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
                <!--指定一个正则表达式  方法名称匹配则将方法作为切入点-->
                <property name="pattern" value=".*save"/>
                <!--指定多个正则表达式  多个表达式用逗号隔开即可-->
                <property name="patterns" value=".*save,.*update"/>
                <!--指定要应用的通知-->
                <property name="advice" ref="advice"/>
        </bean>

        <!---代理Bean-->
        <bean id="studentDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
                <!--指定目标-->
                <property name="target" ref="studentDao"/>
                <!--指定目标实现的接口-->
                <property name="proxyInterfaces" value="com.yh.demo7.StudentDao"/>
                <!--指向Pointcut切入点-->
                <property name="interceptorNames" value="myAdvisor"/>
        </bean>
</beans>
  • 使用默认的切点切面:

整体的配置与正则相同 同样是在代理Bean中指定目标,通知切点之间的关系;

只是需要增加一个表示切点的Bean

<!--切点-->
<bean id="pointcutBean" class="org.springframework.aop.support.NameMatchMethodPointcut">
  <!--指定要增强的方法名称 -->
    <property name="mappedNames">
        <list>
          	<!--*是通配符 标识任意长度的任意内容 与RE无关 如.*表示方法名称带有点的-->
            <value>*save</value>
        </list>
    </property>
</bean>
<!--切面-->
<bean id="myAdvisor2" class="org.springframework.aop.support.DefaultPointcutAdvisor">
    <property name="advice" ref="advice"/>
    <property name="pointcut" ref="pointcutBean"/>
</bean>

 <!---代理Bean-->
<bean id="studentDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
    <!--指定目标-->
    <property name="target" ref="studentDao"/>
    <!--指定目标实现的接口-->
    <property name="proxyInterfaces" value="com.yh.demo7.StudentDao"/>
    <!--指向Pointcut切入点-->
    <property name="interceptorNames" value="myAdvisor2"/>
</bean>

自动生成代理

如果每个Bean都需要配置代理Bean的话,开发维护的工作量将是巨大的;

自动生成代理有三种方式

  • 根据BeanName来查找目标对象并且其生成代理

  • 根据切面信息来查找目标对象并且其生成代理

  • 通过AspectJ注解来指定目标对象(AspectJ中介绍)

基于BeanName生成代理

<!--根据BeanName自动生成代理对象-->

    <!--通知-->
    <bean id="befor" class="com.kkb.demo1.MyAdvice"/>
    <bean id="after" class="com.kkb.demo1.MyAdvice2"/>

    <!--目标-->
    <bean id="userDao1" class="com.kkb.demo1.UserDaoImpl"/>
    <bean id="userDao2" class="com.kkb.demo1.UserDaoImpl"/>

    <!--自动代理生成器-->
    <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
        <property name="interceptorNames" value="after,befor"/>
      	<!--可使用 * 通配符-->
        <property name="beanNames" value="userDao1,userDao2"/>
    </bean>

我们会发现上面的配置中没有与切点相关的信息,的确这种方式定义的切面是普通切面,即所有目标的所有方法都会被增强

基于切点信息生成代理

<!--    根据切面信息自动生成代理对象-->

    <!--通知-->
    <bean id="befor" class="com.kkb.demo1.MyAdvice"/>
    <bean id="after" class="com.kkb.demo1.MyAdvice2"/>

    <!--目标-->
    <bean id="userDao1" class="com.kkb.demo1.UserDaoImpl"/>
    <bean id="userDao2" class="com.kkb.demo1.UserDaoImpl"/>

    <bean class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
        <property name="advice" ref="after"/>
        <property name="pattern" value=".*save"/>
    </bean>
   <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>
</beans>

DefaultAdvisorAutoProxyCreator将会在容器中查找所有Advisor,然后按照re表达式来查找目标对象和切点,最后为目标对象生成代理对象;

两种方式存在一个共同点:都会将容器中的目标对象直接替换为代理对象,这样一来,我们在使用Bean时就不用在考虑获取的是原始对象还是代理对象了,直接使用即可

上述的配置单独拿出一种都是比较简单的,混在一起就很容易乱,你只需要记住,要使用AOP则必须明确的几个关键点及其关系:

  • 目标

    要被增强的Bean 没有什么特殊之处

  • 通知

    要增强的具体代码

  • 切点

    需要明确目标对象中要增强的方法是哪些,pointcut要做的事情

  • 切面

    需要明确在某个切点上应用某些通知,即advisor要做的事情

  • 代理

    需要明确目标

若是普通切面则 只需要明确,目标,和通知即可;

问题:

看起来指定切点的切面比普通切面更强大,那么为什么还需要普通切面呢?

那你设想一下,若你的需求是给所有方法全部加上日志输出,那这时采用普通切面是最简便的方式;

posted @ 2020-01-09 22:46  CoderJerry  阅读(553)  评论(0编辑  收藏