Spring 学习 (三)大话AOP

一直想着怎么去通俗的讲解AOP,这两篇博客给了我启发

https://blog.csdn.net/qukaiwei/article/details/50367761),(https://blog.csdn.net/q982151756/article/details/80513340)下面我加入自己的理解,咱们来说说AOP!

目录

大白话从思想上理解AOP

一、什么是AOP(面向切面编程)?

二、AOP中的相关概念

    1.通知(Advice)

    2.连接点(JoinPoint)

    3.切入点(Pointcut)

    4.切面(Aspect)

    5.引入(introduction)

    6.目标(target)

    7.代理(proxy)

    8.织入(weaving)

我所理解的AOP原理

通过代码来学习

Spring中的通知类型

前置通知:在目标方法执行之前进行操作

后置通知:在目标方法执行之后进行操作

环绕通知:在目标方法执行之前和之后进行操作

异常抛出通知:在程序出现异常的时候,进行操作

最终通知:无论代码是否会有异常,总是会执行

引介通知:(不会用)

Spring切入点的表达式写法

切入点的表达式语法


大白话从思想上理解AOP

 

一、什么是AOP(面向切面编程)?

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

我的大白话:无论在学习或者面试的时候,大家都会张口说spring的特性AOP和IOC(控制反转上一篇有讲),有些大神理解的很到位,但是对于大多数初中级工程师来讲还是模糊阶段,但是为什么会有AOP这种技术呢?(使用AOP的原因,AOP的好处)那就是为了开发者的方便!!!就是为了我们少写代码,省劲!要记住上面我说的话!

然后我们举一个比较容易理解的例子(来自:Spring 之 AOP):

要理解切面编程,就需要先理解什么是切面。用刀把一个西瓜分成两瓣,切开的切口就是切面;炒菜,锅与炉子共同来完成炒菜,锅与炉子就是切面。web层级设计中,web层->网关层->服务层->数据层,每一层之间也是一个切面。编程中,对象与对象之间,方法与方法之间,模块与模块之间都是一个个切面。

我们一般做活动的时候,一般对每一个接口都会做活动的有效性校验(是否开始、是否结束等等)、以及这个接口是不是需要用户登录。

按照正常的逻辑,我们可以这么做。 

这有个问题就是,有多少接口,就要多少次代码copy。对于一个“懒人”,这是不可容忍的。好,提出一个公共方法,每个接口都来调用这个接口。这里有点切面的味道了。 

同样有个问题,我虽然不用每次都copy代码了,但是,每个接口总得要调用这个方法吧。于是就有了切面的概念,我将方法注入到接口调用的某个地方(切点)。

这样接口只需要关心具体的业务,而不需要关注其他非该接口关注的逻辑或处理。 
红框处,就是面向切面编程。

二、AOP中的相关概念

看过了上面的例子,我想大家脑中对AOP已经有了一个大致的雏形,但是又对上面提到的切面之类的术语有一些模糊的地方,接下来就来讲解一下AOP中的相关概念,了解了AOP中的概念,才能真正的掌握AOP的精髓。 

初看这么多术语,一下子都不好接受,慢慢来,很快就会搞懂。

    1.通知(Advice)

  就是你想要的功能,也就是上面说的 安全,事物,日志等。你给先定义好把,然后在想用的地方用一下。

    2.连接点(JoinPoint)

  这个更好解释了,就是spring允许你使用通知的地方,那可真就多了,基本每个方法的前,后(两者都有也行),或抛出异常时都可以是连接点,spring只支持方法连接点.其他如aspectJ还可以让你在构造器或属性注入时都行,不过那不是咱关注的,只要记住,和方法有关的前前后后(抛出异常),都是连接点。

    3.切入点(Pointcut)

  上面说的连接点的基础上,来定义切入点,你的一个类里,有15个方法,那就有几十个连接点了对把,但是你并不想在所有方法附近都使用通知(使用叫织入,以后再说),你只想让其中的几个,在调用这几个方法之前,之后或者抛出异常时干点什么,那么就用切点来定义这几个方法,让切点来筛选连接点,选中那几个你想要的方法。

    4.切面(Aspect)

  切面是通知和切入点的结合。现在发现了吧,没连接点什么事情,连接点就是为了让你好理解切点,搞出来的,明白这个概念就行了。通知说明了干什么和什么时候干(什么时候通过方法名中的before,after,around等就能知道),而切入点说明了在哪干(指定到底是哪个方法),这就是一个完整的切面定义。

    5.引入(introduction)

  允许我们向现有的类添加新方法属性。这不就是把切面(也就是新方法属性:通知定义的)用到目标类中吗

    6.目标(target)

  引入中所提到的目标类,也就是要被通知的对象,也就是真正的业务逻辑,他可以在毫不知情的情况下,被咱们织入切面。而自己专注于业务本身的逻辑。

    7.代理(proxy)

  怎么实现整套aop机制的,都是通过代理,这个一会给细说。

    8.织入(weaving)

  把切面应用到目标对象来创建新的代理对象的过程。有3种方式,spring采用的是运行时,为什么是运行时,后面解释。

  关键就是:切点定义了哪些连接点会得到通知

我所理解的AOP原理

spring用代理类包裹切面,把他们织入到Spring管理的bean中。也就是说代理类伪装成目标类,它会截取对目标类中方法的调用,让调用者对目标类的调用都先变成调用伪装类,伪装类中就先执行了切面,再把调用转发给真正的目标bean。

  现在可以自己想一想,怎么搞出来这个伪装类,才不会被调用者发现(过JVM的检查,JAVA是强类型检查,哪里都要检查类型)。

  1.实现和目标类相同的接口,我也实现和你一样的接口,反正上层都是接口级别的调用,这样我就伪装成了和目标类一样的类(实现了同一接口,咱是兄弟了),也就逃过了类型检查,到java运行期的时候,利用多态的后期绑定(所以spring采用运行时),伪装类(代理类)就变成了接口的真正实现,而他里面包裹了真实的那个目标类,最后实现具体功能的还是目标类,只不过伪装类在之前干了点事情(写日志,安全检查,事物等)。

  这就好比,一个人让你办件事,每次这个时候,你弟弟就会先出来,当然他分不出来了,以为是你,你这个弟弟虽然办不了这事,但是他知道你能办,所以就答应下来了,并且收了点礼物(写日志),收完礼物了,给把事给人家办了啊,所以你弟弟又找你这个哥哥来了,最后把这是办了的还是你自己。但是你自己并不知道你弟弟已经收礼物了,你只是专心把这件事情做好。

  顺着这个思路想,要是本身这个类就没实现一个接口呢,你怎么伪装我,我就压根没有机会让你搞出这个双胞胎的弟弟,那么就用第2种代理方式,创建一个目标类的子类,生个儿子,让儿子伪装我

  2.生成子类调用,这次用子类来做为伪装类,当然这样也能逃过JVM的强类型检查,我继承的吗,当然查不出来了,子类重写了目标类的所有方法,当然在这些重写的方法中,不仅实现了目标类的功能,还在这些功能之前,实现了一些其他的(写日志,安全检查,事物等)。

  这次的对比就是,儿子先从爸爸那把本事都学会了,所有人都找儿子办事情,但是儿子每次办和爸爸同样的事之前,都要收点小礼物(写日志),然后才去办真正的事。当然爸爸是不知道儿子这么干的了。这里就有件事情要说,某些本事是爸爸独有的(final的),儿子学不了,学不了就办不了这件事,办不了这个事情,自然就不能收人家礼了。

  前一种兄弟模式,spring会使用JDK的java.lang.reflect.Proxy类,它允许Spring动态生成一个新类来实现必要的接口,织入通知,并且把对这些接口的任何调用都转发到目标类。

  后一种父子模式,spring使用CGLIB库生成目标类的一个子类,在创建这个子类的时候,spring织入通知,并且把对这个子类的调用委托到目标类。

  相比之下,还是兄弟模式好些,他能更好的实现松耦合,尤其在今天都高喊着面向接口编程的情况下,父子模式只是在没有实现接口的时候,也能织入通知,应当做一种例外。

那在继续学习之前,什么是代理模式以及上述两种AOP动态代理的底层实现都在这篇博客中,我强烈建议先看完,不然很难理解后面的内容:https://blog.csdn.net/qq_28863191/article/details/101517631

让我们通过代码来学习把:

1.导入aop所需要的对应Jar包

一会我们需要使用JUnit来测试,所以需要导入Spring整合单元测试的jar包

2.然后在配置文件 application.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">

</beans>

横线部分就是aop的约束,我们学习到这里发现什么了么,没错,引入新的约束在多数情况下是添加一个xmlns:XXX的变量,然后在 xsi:schemaLocation 中加入其对应的 url,比如 aop 就是添加

----http://www.springframework.org/schema/aop 
----http://www.springframework.org/schema/aop/spring-aop.xsd

3.接着,我们就要开始写接口和实现类了

/**
 * 一个产品的接口
 * @author hp
 *
 */
public interface ProductDAO {
	public void save();
	public void update();
	public void find();
	public void delete();
}
/**
 * 产品接口的实现类
 * @author hp
 *
 */
public class ProductDAOImpl implements ProductDAO {

	@Override
	public void save() {
		// TODO Auto-generated method stub
		System.out.println("保存商品。。。");
	}

	@Override
	public void update() {
		// TODO Auto-generated method stub
		System.out.println("修改商品。。。");

	}

	@Override
	public void find() {
		// TODO Auto-generated method stub
		System.out.println("查找商品。。。");
	}

	@Override
	public void delete() {
		// TODO Auto-generated method stub
		System.out.println("删除商品。。。");
	}

}

4.定义一个切面类,将通知(你所需要插入的功能)写成方法:

/**
 * 定义切面类,它有一个checkPrice的权限校验方法
 * @author hp
 *
 */
public class MyAspectXML {

	public void checkPrice() {
		
		System.out.println("权限校验==========");
	}
}

5.我们将产品类和切面类都交给Spring管理

	<!-- 配置目标对象,被增强对象=================== -->
	<bean id="productDao" class="com.ysx.spring.demo2.ProductDAOImpl" />
	
	<!-- 将切面类交给Spring -->
	<bean id="myAspect" class="com.ysx.spring.demo2.MyAspectXML" />

    <!-- 通过AOP的配置完成对目标类产生的代理  -->
	<aop:config>
        <!-- 设置一个切入点在表达式execution(* com.ysx.spring.demo2.ProductDAOImpl.save(..))这里 -->
		<aop:pointcut expression="execution(* com.ysx.spring.demo2.ProductDAOImpl.save(..))" id="pointcut1"/>
		<!-- 配置切面 -->
		<aop:aspect ref="myAspect">
            <!-- aop:before表示在表达式之前
                 pointcut-ref对应着ID所代表的表达式 -->
			<aop:before method="checkPrice" pointcut-ref="pointcut1"/>
		</aop:aspect>
	</aop:config>

6.测试

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

//RunWith是固定格式
@RunWith(SpringJUnit4ClassRunner.class)

//加载配置文件
@ContextConfiguration("classpath:applicationContext.xml")
public class SpringDemo2 {

    //注入到需要被增强的类
	@Resource(name="productDao")
	private ProductDAO productDao;
	
	@Test
	public void demo2() {
		productDao.save();
	}
}

可以在自己的IDE上试一下,得到的结果是

Spring中的通知类型

前置通知:在目标方法执行之前进行操作

前置通知可以获得切入点信息

在切面类加入

public class MyAspectXML {

	public void checkPrice(JoinPoint joinPoint) {
		
		System.out.println("权限校验==========" + joinPoint);
	}
}

结果就会变成

后置通知:在目标方法执行之后进行操作

后置通知除了可以获得切入点信息之外,还可以获得方法的返回值

 

这次我们拿 update() 方法举例

接口和实现类

/**
 * 一个产品的接口
 * @author hp
 *
 */
public interface ProductDAO {
	public void save();
	public void update();
	public void find();
	public String delete();
}


/**
 * 产品接口的实现类
 * @author hp
 *
 */
public class ProductDAOImpl implements ProductDAO {

	@Override
	public void save() {
		// TODO Auto-generated method stub
		System.out.println("保存商品。。。");
	}

	@Override
	public void update() {
		// TODO Auto-generated method stub
		System.out.println("修改商品。。。");

	}

	@Override
	public void find() {
		// TODO Auto-generated method stub
		System.out.println("查找商品。。。");
	}

	@Override
	public String delete() {
		// TODO Auto-generated method stub
		System.out.println("删除商品。。。");
		return "杨大侠";
	}

}

切面类

public class MyAspectXML {
	/**
	 * 前置通知
	 * @param joinPoint
	 */
	public void checkPrice(JoinPoint joinPoint) {
		System.out.println("权限校验==========" + joinPoint);
	}
	
	/**
	 * 后置通知
	 * @param r
	 */
	public void writeLog(Object result) {
		System.out.println("日志记录==========" + result);
	}
}

application.xml配置文件

<aop:config>
		<aop:pointcut expression="execution(* com.ysx.spring.demo2.ProductDAOImpl.save(..))" id="pointcut1"/>
        <!-- 配置连接到delete方法 -->
		<aop:pointcut expression="execution(* com.ysx.spring.demo2.ProductDAOImpl.delete(..))" id="pointcut2"/>
		<!-- 配置切面 -->
		<aop:aspect ref="myAspect">
			<aop:before method="checkPrice" pointcut-ref="pointcut1"/>
            <!-- pointcut-ref 通过上面id连接
                 method       表示要连接的切面类里面的通知方法
                 returning    里面是返回值,这个变量的名字必须和writeLog方法的参数一致 -->
			<aop:after-returning method="writeLog" pointcut-ref="pointcut2" returning="result"/>
		</aop:aspect>
	</aop:config>

测试

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class SpringDemo2 {

	@Resource(name="productDao")
	private ProductDAO productDao;
	
	@Test
	public void demo2() {
		productDao.save();
		productDao.delete();
	}
}

最后是我们得到的结果,可以看到“杨大侠”名字在日志记录的时候被输出出来了。

环绕通知:在目标方法执行之前和之后进行操作

环绕通知除了可以获得切入点信息和方法的返回值之外,还可以阻止目标方法的执行

这次我们拿 update() 方法举例

接口和实现类

/**
 * 一个产品的接口
 * @author hp
 *
 */
public interface ProductDAO {
	public void save();
	public void update();
	public void find();
	public String delete();
}

/**
 * 产品接口的实现类
 * @author hp
 *
 */
public class ProductDAOImpl implements ProductDAO {

	@Override
	public void save() {
		// TODO Auto-generated method stub
		System.out.println("保存商品。。。");
	}

	@Override
	public void update() {
		// TODO Auto-generated method stub
		System.out.println("修改商品。。。");

	}

	@Override
	public void find() {
		// TODO Auto-generated method stub
		System.out.println("查找商品。。。");
		int i = 1/0;
	}

	@Override
	public String delete() {
		// TODO Auto-generated method stub
		System.out.println("删除商品。。。");
		return "杨大侠";
	}

}

切面类

/**
 * 定义切面类,它有一个checkPrice的权限校验方法
 * @author hp
 *
 */
public class MyAspectXML {
	/**
	 * 前置通知
	 * @param joinPoint
	 */
	public void checkPrice(JoinPoint joinPoint) {
		System.out.println("权限校验==========" + joinPoint);
	}
	
	/**
	 * 后置通知
	 * @param r
	 */
	public void writeLog(Object result) {
		System.out.println("日志记录==========" + result);
	}
	
	public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
		System.out.println("环绕前通知============");
		//这里就相当于执行目标程序,如果有返回值接收返回,没有就void
		Object obj = joinPoint.proceed();
		System.out.println("环绕前通知============");
		return obj;
	}
	
	/**
	 * 异常抛出通知
	 */
	public void afterThrowing(Throwable ex) {
		System.out.println("异常抛出通知===============" + ex);
	}
	
	public void after() {
		System.out.println("最终通知==============");
	}
}

application.xml配置文件

<!-- 通过AOP的配置完成对目标类产生的代理  -->
	<aop:config>
		<aop:pointcut expression="execution(* com.ysx.spring.demo2.ProductDAOImpl.save(..))" id="pointcut1"/>
		<aop:pointcut expression="execution(* com.ysx.spring.demo2.ProductDAOImpl.delete(..))" id="pointcut2"/>
		<aop:pointcut expression="execution(* com.ysx.spring.demo2.ProductDAOImpl.update(..))" id="pointcut3"/>
		<aop:pointcut expression="execution(* com.ysx.spring.demo2.ProductDAOImpl.find(..))" id="pointcut4"/>
		<!-- 配置切面 -->
		<aop:aspect ref="myAspect">
			<!-- 前置通知=========== -->
			<aop:before method="checkPrice" pointcut-ref="pointcut1"/>
			<!-- 后置通知=========== -->
			<aop:after-returning method="writeLog" pointcut-ref="pointcut2" returning="result"/>
			<!-- 环绕通知=========== -->
			<aop:around method="around" pointcut-ref="pointcut3"/>
			<!-- 异常抛出通知=========== -->
			<aop:after-throwing method="afterThrowing" pointcut-ref="pointcut4" throwing="ex"/>
			<!-- 最终通知 -->
			<aop:after method="after" pointcut-ref="pointcut4"/>
		</aop:aspect>
	</aop:config>

测试

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class SpringDemo2 {

	@Resource(name="productDao")
	private ProductDAO productDao;
	
	@Test
	public void demo2() {
		productDao.save();
		productDao.delete();
		productDao.update();
		productDao.find();
	}
}

运行结果

异常抛出通知:在程序出现异常的时候,进行操作

可以获得异常信息

最终通知:无论代码是否会有异常,总是会执行

相当于执行 finally 代码块里的内容,跟异常抛出通知一起讲,相当于try/catch/finally

这次我们拿 find() 方法举例

接口和实现类

/**
 * 一个产品的接口
 * @author hp
 *
 */
public interface ProductDAO {
	public void save();
	public void update();
	public void find();
	public String delete();
}


/**
 * 产品接口的实现类
 * @author hp
 *
 */
public class ProductDAOImpl implements ProductDAO {

	@Override
	public void save() {
		// TODO Auto-generated method stub
		System.out.println("保存商品。。。");
	}

	@Override
	public void update() {
		// TODO Auto-generated method stub
		System.out.println("修改商品。。。");

	}

	@Override
	public void find() {
		// TODO Auto-generated method stub
		System.out.println("查找商品。。。");
	}

	@Override
	public String delete() {
		// TODO Auto-generated method stub
		System.out.println("删除商品。。。");
		return "杨大侠";
	}

}

切面类

public class MyAspectXML {
	/**
	 * 前置通知
	 * @param joinPoint
	 */
	public void checkPrice(JoinPoint joinPoint) {
		System.out.println("权限校验==========" + joinPoint);
	}
	
	/**
	 * 后置通知
	 * @param r
	 */
	public void writeLog(Object result) {
		System.out.println("日志记录==========" + result);
	}
	
	public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
		System.out.println("环绕前通知============");
		//这里就相当于执行目标程序,如果有返回值接收返回,没有就void
		Object obj = joinPoint.proceed();
		System.out.println("环绕前通知============");
		return obj;
	}
}

application.xml配置文件

	<aop:config>
		<aop:pointcut expression="execution(* com.ysx.spring.demo2.ProductDAOImpl.save(..))" id="pointcut1"/>
		<aop:pointcut expression="execution(* com.ysx.spring.demo2.ProductDAOImpl.delete(..))" id="pointcut2"/>
		<aop:pointcut expression="execution(* com.ysx.spring.demo2.ProductDAOImpl.update(..))" id="pointcut3"/>
		<!-- 配置切面 -->
		<aop:aspect ref="myAspect">
			<!-- 前置通知=========== -->
			<aop:before method="checkPrice" pointcut-ref="pointcut1"/>
			<!-- 后置通知=========== -->
			<aop:after-returning method="writeLog" pointcut-ref="pointcut2" returning="result"/>
			<!-- 环绕通知=========== -->
			<aop:around method="around" pointcut-ref="pointcut3"/>
		</aop:aspect>
	</aop:config>

测试

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class SpringDemo2 {

	@Resource(name="productDao")
	private ProductDAO productDao;
	
	@Test
	public void demo2() {
		productDao.save();
		productDao.delete();
		productDao.update();
	}
}

运行结果

引介通知:(不会用)

 

Spring切入点的表达式写法

切入点的表达式语法

基于execution的函数完成的

语法:

  • [访问修饰符]  方法返回值  包名.类名.方法名(参数)(注:[]括号表示可以省略)
  • public void com.ysx.spring.CustomerDAO.save(..)   (参数位置两个点表示任意参数)
  • 我们还可以用 * 表示, * 代表任意类型,比如在上面通知的代码中 用 * 表示任意返回类型
  • * com.ysx.spring.CustomerDAO. *(..) 表示CustomerDAO中的每一个方法,返回类型任意
  • * com.ysx.spring.*.*(..)表示com.ysx.spring包下的每一个类的每一个方法
  • * com.ysx.spring.*DAO.*(..)表示com.ysx.spring包下的每一个叫 XXXDAO 的类的每一个方法     
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值