(3)-Spring之AOP(传统),动态代理与CGLib笔记

Spring整合单元测试

在前面的Spring案例中,需要创建ApplicationContext对象,然后调用getBean获取需要测试的Bean。如下:

 ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
 PersonController bean = context.getBean(PersonController.class);

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

在pom.xml中添加依赖

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

测试案例:

@RunWith(SpringJUnit4ClassRunner.class)//固定写法
@ContextConfiguration("classpath:applicationContext.xml")//指定要加载的配置文件
public class MyTest1 {
    @Resource(name = "personController")//使用id获取Bean
    private PersonController personController;
    @Test
    public void test1(){
        personController.haha();
    }
}

下面是今天的重点学习内容

AOP概念

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

说了一大堆我也没清楚说的啥。。。。。。

为什么需要AOP?

在项目开发中会有一些通用需求,例如

  • 权限控制
  • 日志输出
  • 事务管理
  • 数据统计 and so on
    看似简单,开发麻烦
    来个例子
    在PersonDao中有以下几个方法
public class PersonDao {
    public void haha() {
        System.out.println("哈哈哈哈哈哈哈");
    }
    public void select(){
        System.out.println("查询!");
    }
    public void update(){
        System.out.println("更新!");
    }
    public void delete(){
        System.out.println("删除!");
    }
}

后来需求有变化,需要给update和delete两个方法输出执行时间统计与日志分析

这有何难?就是加几行代码的事

public class PersonDao {
    public void haha() {
        System.out.println("哈哈哈哈哈哈哈");
    }

    public void select() {
        System.out.println("查询!");
        //结束时间
    }

    public void update() {
        //初始时间
        Long begin = new Date().getTime();
        System.out.println("更新!");
        //结束时间
        Long end = new Date().getTime();
        long time = end - begin;
        System.out.println(time);
    }

    public void delete() {
        //初始时间
        Long begin = new Date().getTime();
        System.out.println("删除!");
        //结束时间
        Long end = new Date().getTime();
        long time = end - begin;
        System.out.println(time);
    }
}

喏~这不是很简单?
但是!!!注意是但是!!!
聪明的你忍受得了这么。。。。。吧,而且上述代码存在不少问题

  • 修改了源码,违背了OCP
  • 如果·有大量这种类似的方法,重复代码太多

AOP来解决

在不修改源代码以及调用方式的情况下扩展新功能

而由于需要扩展的方法有很多,于是把这些方法命名为切面

即切面就是一系列需要扩展功能的方法的集合

使用AOP的目的

  • 日志记录
  • 性能统计
  • 安全控制
  • 事务处理
  • 异常处理
  • 等等…
    将这些重复代码从业务逻辑代码中划分出来,这样它们就可以独立到非业务逻辑的方法中

从而改变这些行为的时候不会影响业务逻辑的代码

AOP开发的相关术语介绍

  • Joinpoint(连接点):所谓连接点是指那些被拦截到的点,可以理解为是扩展内容与原有内容交互的点,可以理解为可被扩展的地方。在 spring 中,这些点指的是方法,因为 spring 只支持方法类型的连接点。
    案例中的四个方法
  • pointcut(切点):指的是要被扩展(增加了功能)的内容,包括方法或属性(joinpoint)
    案例中两个增加了新功能的方法
  • advice(通知):指的是要在切点上增加的功能
    案例中输出执行时间的功能。

通知分为前置通知,后置通知,异常通知,最终通知,环绕通知(切面要完成的功能),引介通知(指的是在不修改类代码的前提下,为类增加方法或属性,了解即可)

  1. target(目标):就是要应用通知的对象,即要被增强的对象
    案例中的PersonDao
  2. weaving(织入):还是一个动词,描述的是将扩展功能应用到target的这个过程
    案例中修改源代码的过程
  3. proxy(代理):Spring使用代理来完成AOP,对某个对象增强后,就得到一个代理对象。即一个类被 AOP 织入增强后,就产生一个结果代理类
    Spring AOP的整个过程就是对target应用advice最后产生proxy,我们最后使用的都是proxy对象
  4. aspect(切面)切点与通知的结合,是一个抽象概念
    案例中update和delete(切点)、通知(输出时间差),共同组成一个切面。

AOP的传统实现

如图所示:
在这里插入图片描述
具体实现如下:
dao接口:

@Repository
public interface PersonDao {
    void haha();

    void select();

    void update();

    void delete();
}

代理类

public class MyProxy implements PersonDao {
    private PersonDao target;

    public MyProxy() {
    }

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

    @Override
    public void haha() {
        target.haha();
    }

    @Override
    public void select() {
        target.select();
    }

    @Override
    public void update() {
        //初始时间
        Long begin = new Date().getTime();
        target.update();
        //结束时间
        Long end = new Date().getTime();
        long time = end - begin;
        System.out.println("消耗时间:" + time);
    }

    @Override
    public void delete() {
        //初始时间
        Long begin = new Date().getTime();
        target.delete();
        //结束时间
        Long end = new Date().getTime();
        long time = end - begin;
        System.out.println("消耗时间:" + time);
    }
}

实现类

@Repository
public class PersonDaoImpl implements PersonDao {
    public void haha() {
        System.out.println("哈哈哈哈哈哈哈");
    }

    public void select() {
        System.out.println("查询!");
    }

    public void update() {
        System.out.println("更新!");
    }

    public void delete() {
        System.out.println("删除!");
    }
}

测试:

@RunWith(SpringJUnit4ClassRunner.class)//固定写法
@ContextConfiguration("classpath:applicationContext.xml")//指定要加载的配置文件
public class MyTest1 {
    private PersonDao personDao = new MyProxy(new PersonDaoImpl());
    @Test
    public void test2(){
        personDao.update();
    }
}

是啊,确实解决了问题,也没有违背OCP原则,但是如果有大量的方法每个方法就要写一次通知,造成代码冗余。

所以官方动态代理给出了解决办法

AOP的底层实现

Spring 的 AOP 的底层用到两种代理机制:

  1. JDK 的动态代理(官方) :针对实现了接口的类产生代理。

  2. Cglib 的动态代理 (民间):针对没有实现接口的类产生代理。应用的是底层的字节码增强的技术生成当前类的子类对象。

动态代理(官方)

JDK1.4出现

这种代理机制的结构:
在这里插入图片描述
上述代理类可以写成:

package cx;

import cx.dao.PersonDao;
import cx.dao.PersonDaoImpl;

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

/**
 *
 */
public class MyProxy2 implements InvocationHandler {
    private PersonDao target;

    public MyProxy2() {
    }

    public MyProxy2(PersonDao target) {
        this.target = target;
    }

    //创建获取代理对象方法,本质是动态的产生一个target对象的接口实现类
    public Object getProxy(){
        //getClassLoader类加载器,动态帮我们生成类
        //getInterfaces 实现的接口有哪些
        //this 是代理方法有谁来做,也就是实现InvocationHandler接口的类,因为本类实现了,所以用tjis
        Object o = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
        return o;
    }

    /**
     *
     * @param proxy  代理对象
     * @param method 要执行的方法
     * @param args    参数列表
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //初始时间
        Long begin = new Date().getTime();
        //调用原始方法
        Object invoke = method.invoke(target, args);
        Thread.sleep(2000);
        //结束时间
        Long end = new Date().getTime();
        long time = end - begin;
        System.out.println("消耗时间:" + time);
        //返回原始方法执行结果
        return invoke;
    }

    public static void main(String[] args) {
        PersonDao personDao = (PersonDao) new MyProxy2(new PersonDaoImpl()).getProxy();
        personDao.update();
    }
}

		//获取类信息和方法名
        String className = target.getClass().getName();
        String methodName = method.getName();

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

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

CGLib

CGLib(字节码生成库)是第三方库,需要添加依赖

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

这种代理机制的结构:
在这里插入图片描述
接口和实现类和上面的动态代理一样,分别是PersonDao和PersonDaoImpl
不同的是CGLib类。如下:

public class CGLibProxy implements MethodInterceptor {
    private PersonDao target;

    public CGLibProxy() {
    }

    public CGLibProxy(PersonDao target) {
        this.target = target;
    }

    //获取代理对象方法,本质是动态的产生一个target对象的接口实现类
    public Object getProxy() {
        //增强类 CGLib核心类
        Enhancer enhancer = new Enhancer();
        //设置代理类的父类
        enhancer.setSuperclass(target.getClass());
        //设置方法回调 当有人调用代理对象就会调用这个方法,即代理调用代理对象方法时会执行的方法
        enhancer.setCallback(this);
        //产生一个代理对象
        Object o = enhancer.create();
        return o;
    }

    /**
     *
     * @param proxy        代理对象
     * @param method        外界要执行的方法
     * @param objects       传递的参数
     * @param methodProxy   方法代理对象,用于执行父类(目标)方法
     * @return				原始方法的返回值
     * @throws Throwable
     */
    @Override
    public Object intercept(Object proxy, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        //添加额外的逻辑
        System.out.println("方法名:"+method.getName());
        //调用原始的方法
        Object o = methodProxy.invokeSuper(proxy, objects);
        return o;
    }

    public static void main(String[] args) {
        PersonDao personDao = (PersonDao) new CGLibProxy(new PersonDaoImpl()).getProxy();
        personDao.update();
    }
}

注意:

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

Spring采用的就是上述实现AOP

Spring中的AOP

Spring在运行期间,可以自动生成动态代理对象,不需要特殊的编译器。

Spring AOP的底层就是通过JDK动态代理和CGLib动态代理技术,为目标Bean执行横向织入。
Spring会自动选择代理方式
1.若目标对象实现了若干接口,Spring使用JDK的java.lang.reflect.Proxy类代理
2.若目标对象没有实现任何接口,Spring使用CGLib库生成目标对象的子类

即:

  • 当目标编写接口时,aop底层采用jdk动态代理 。
  • 当目标类不写接口时,aop底层采用cglib动态代理。

Spring的通知类型

  • 前置 org.springframework.aop.MethodBeforeAdvice 在目标方法执行之前执行.

  • 后置 org.springframework.aop.AfterReturningAdvice 在目标方法执行之后执行

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

  • 异常 org.springframework.aop.ThrowsAdvice 在目标方法执行出现异常的时候执行

  • 最终:org.springframework.aop.AfterAdvice 无论目标方法是否出现异常最终通知都会执行.

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

性(非重点)

注意:

最终通知和后置通知的区别

  • 最终通知是在目标方法执行之后执行的通知,无论如何都会在目标方法调用过后执行,即使目标方法没有正常的执行完成。
  • 后置通知在方法正常执行后执行,如果没有正常执行,例如抛出异常则后置通知不会执行,后置通知可以通过配置得到返回值,而最终通知不可以

Spring切面类型

普通切面:
指的是未指定具体切入点的切面,将把目标对象所有方法作为切入点
接口:

public interface PersonDao {
    void haha();

    void select();

    void update();

    void delete();
}

实现类:

public class PersonDaoImpl implements PersonDao {
    public void haha() {
        System.out.println("哈哈哈哈哈哈哈");
    }

    public void select() {
        System.out.println("查询!");
    }

    public void update() {
        System.out.println("更新!");
    }

    public void delete() {
        System.out.println("删除!");
    }
}

通知类:

package cx;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;

import java.lang.reflect.Method;

/**
 *
 */
public class MyAdvice implements MethodInterceptor, MethodBeforeAdvice, AfterReturningAdvice {
    @Override
    public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
        System.out.println("后置通知");
    }

    @Override
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        System.out.println("前置通知");
    }


    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        System.out.println("环绕前");
        Object proceed = methodInvocation.proceed();
        System.out.println("环绕后");
        return proceed;
    }
}

这样就完成了?
重点在这里,不要忘记在applicationContext.xml中进行配置

 	<!--目标:实现类-->
    <bean id="personDaoImpl" class="cx.dao.PersonDaoImpl" />
    <!--通知:通知类-->
    <bean id="advice" class="cx.MyAdvice"/>
    <!--创建代理对象-->
    <bean id="factoryPersonBean" class="org.springframework.aop.framework.ProxyFactoryBean">
        <!--指定通知-->
        <property name="interceptorNames" value="advice"/>
        <!--指定目标类-->
        <property name="target" ref="personDaoImpl"/>
    </bean>

上面方式对目标类的方法全部增强了,但是我们有时候只想增强其中的一个或几个方法,不想增强整个类,又该怎么办呢?

切入点切入面使用( PointcutAdvisor )
指定为目标对象中仅某些内容进行增强

  • 使用正则匹配方法的切面
<!--希望对方法加以区分,仅对目标对象的部分方法进行增强-->
    <!--切入点切面使用-->
    <!--目标:实现类-->
    <bean id="personDaoImpl" class="cx.dao.PersonDaoImpl" />
    <!--通知:通知类-->
    <bean id="advice" class="cx.MyAdvice"/>
    <!--通过正则表达式来匹配,从而确定切点-->
    <bean id="pointcutAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
        <!--指定一个正则表达式,匹配方法名  包名加类名加方法名-->
        <property name="patterns" value=".*update"/>
        <!--指定通知-->
        <property name="advice" ref="advice"/>
    </bean>
    <!--创建代理Bean-->
    <bean id="personDao" class="org.springframework.aop.framework.ProxyFactoryBean">
        <!--指定pointcut切点-->
        <property name="interceptorNames" value="pointcutAdvisor"/>
        <!--指定目标类-->
        <property name="target" ref="personDaoImpl"/>
    </bean>

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

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

	<!--目标:实现类-->
    <bean id="personDaoImpl" class="cx.dao.PersonDaoImpl" />
    <!--通知:通知类-->
    <bean id="advice" class="cx.MyAdvice"/>
    <!--切点
		表示切点的Bean
	-->
    <bean id="pointcutBean" class="org.springframework.aop.support.NameMatchMethodPointcut">
        <!--指定要增强的方法名称-->
        <property name="mappedNames" >
            <list>
                <!--*是通配符-->
                <value>*update</value>
                <value>*delete</value>
            </list>
        </property>
    </bean>
    <!--组织切面信息-->
    <bean id="pointcutAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
        <!--匹配-->
        <property name="pointcut" ref="pointcutBean"/>
        <!--通知-->
        <property name="advice" ref="advice"/>
    </bean>
    <!--代理对象-->
    <bean id="personDao" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="target" ref="personDaoImpl"/>
        <property name="interceptorNames" value="pointcutAdvisor"/>
    </bean>

自动生成代理

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

自动生成代理由三种方式

  • 根据BeanName来查找目标对象并生成代理
  • 根据切面信息来查找目标对象并且生成代理
  • 通过AspectJ注解来指定目标对象
    根据BeanName来生成代理
    很少用
<!--根据BeanName生成代理-->
    <!--目标-->
    <bean id="personDao" class="cx.dao.PersonDaoImpl"/>
    <bean id="personDao2" class="cx.dao.PersonDaoImpl2"/>
    <!--通知-->
    <bean id="advice" class="cx.MyAdvice"/>
    <bean id="advice2" class="cx.MyAdvice"/>
    <!--BeanName-->
    <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
        <!--通知-->
        <property name="interceptorNames" value="advice,advice2"/>
        <!--切点-->
        <!--可使用*通配符-->
        <property name="beanNames" value="personDao,personDao2"/>
    </bean>

可以发现并未配置与切点相关的信息,定义的为普通切面,也就是目标中所有的方法都会增强

调用时根据BeanName中进行调用

@Resource(name = "personDao")
    private PersonDao personDao;

根据切点信息生成代理

	<!--根据切点信息生成代理-->

    <!--目标-->
    <bean id="personDao" class="cx.dao.PersonDaoImpl"/>
    <!--通知-->
    <bean id="advice" class="cx.MyAdvice"/>
    <!--组织切面信息-->
    <bean class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
        <!--指定一个正则表达式,匹配方法名-->
        <property name="patterns" value=".*update"/>
        <!--指定通知-->
        <property name="advice" ref="advice"/>
    </bean>
    <!--代理Bean-->
    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>

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

两种方式存在一个共同点

都会将容器中额目标对象直接替换成代理对象,在使用Bean的时候就不需要考虑获取的是原始对象还是代理对象,只管用。

使用AOP的关键点及关系

  • 目标
    要被增强的Bean
  • 通知
    要增强的代码,就是你要扩展的功能
  • 切点
    明确目标对象要增强的方法是哪些,pointcut要做的事
  • 切面
    需要在某个切点上应用某些通知,即Advisor要做的事
  • 代理
    需要明确目标

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值