如果说 IoC 是 Spring 的核心,那么面向切面编程就是 Spring 最为重要的功能之一了,在数据库事务中切面编程被广泛使用。
面向切面编程,通过预编译和动态代理实现程序功能的统一维护的一种技术,主要功能:日志记录,性能统计,安全控制,事务处理,异常处理等
AOP能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
实际使用SpringAOP之前,了解他的概念是必不可少的一个过程,
SpringAOP主要围绕以下概念展开:
切面[Aspect]:一个关注点的模块化,这个关注点可能会横切多个对象
连接点[Joinpoint]:程序执行过程中某个特定的连接点
通知[Advice]:在切面的某个特的连接点上执行的动作
切入点[Pointcut]:匹配连接的断言,在Aop中通知和一个切入点表达式关联
引入[Intruduction]:在不修改类代码的前提下,为类添加新的方法和属性
目标对象[Target Object]:被一个或者多个切面所通知的对象
Aop代理[AOP Proxy]:AOP框架创建的对象,用来实现切面契约(aspect contract)(包括方法执行等)
织入[Weaving]:把切面连接到其他的应用程序类型或者对象上,并创建一个被通知的对象,氛围:编译时织入,类加载时织入,执行时织入
前置通知[before advice]在某个连接点(jion point)之前执行的通知,但不能阻止连接点前的执行(除非抛出一个异常)
正常返回后通知[after returning advice]在某个连接点(jion point)正常执行完后执行通知
抛出异常通知[after throwing advice] 在方法异常退出时执行的通知
返回通知[after(finally) advice]在方法抛出异常退出时候的执行通知(不管正常返回还是异常退出)
环绕通知[around advice]包围一个连接点(jion point)的通知
例如定义切入点表达式
execution(* com.sample.service.impl..*.*(..))
execution()是最常用的切点函数,其语法如下所示:
整个表达式可以分为五个部分:
1、execution(): 表达式主体。 2、第一个 * 号:表示返回类型,* 号表示所有的类型。 3、包名:表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包,com.sample.service.impl包、子孙包下所有类的方法。 4、第二个 * 号:表示类名,* 号表示所有的类。 5、*(..):最后这个星号表示方法名,* 号表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数。
execution(public * *(..)) //切入点为执行所有的public方式时execution(* set*(..)) //切入点执行所有的set开始的方法时execution(* com.xyz.service.Account.*(..)) //切入点执行Account类的所有方法时execution(* com.xyz.service.*.*(..)) //切入点执行com.xyz.service包下的所有方法时execution(* com.xyz.service..*.*(..)) //切入点执行com.xyz.service包以及其子包的所有的方法时
上边这种方式aspectj和springaop通用的,其他方式可以自己查找资料
为了更好的说明 AOP 的概念,我们来举一个实际中的例子来说明:
在上面的例子中,包租婆的核心业务就是签合同,收房租,那么这就够了,灰色框起来的部分都是重复且边缘的事,交给中介商就好了,这就是 AOP 的一个思想:让关注点代码与业务代码分离!
1.业务主体类,就是现在对应的包猪婆的业务逻辑。
package pojo;
import org.springframework.stereotype.Component;
@Component("landlord")public class Landlord {
public void service() { // 仅仅只是实现了核心的业务功能 System.out.println("签合同"); System.out.println("收房租"); }}
2.Broker 主要是上面所对应的中间商业务处理类,它任务就是带租客看房以及谈价格,其中@Before和@After是在包猪婆业务的前后分别做好相应的售前和售后的工作(其实这里可以用一个环绕通知来弄,看后面的代码)。
package aspect;
import org.aspectj.lang.annotation.After;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.springframework.stereotype.Component;
@Component@Aspectclass Broker {
@Before("execution(* pojo.Landlord.service())") public void before(){ System.out.println("带租客看房"); System.out.println("谈价格"); }
@After("execution(* pojo.Landlord.service())") public void after(){ System.out.println("交钥匙"); }}
3.在 applicationContext.xml 中配置自动注入,并告诉 Spring IoC 容器去哪里扫描这两个 Bean:
<?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 http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<context:component-scan base-package="aspect" /> <context:component-scan base-package="pojo" />
<aop:aspectj-autoproxy/></beans>
4.测试代码,运行包猪婆的主体服务类,可以看到日志会将中间商以及包猪婆的所有工作都打印了出来组成了一个完成的租房流程。
package test;
import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;import pojo.Landlord;
public class TestSpring {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); Landlord landlord = (Landlord) context.getBean("landlord", Landlord.class); landlord.service();
}}
5.执行看到效果:
这个例子使用了一些注解,现在看不懂没有关系,但我们可以从上面可以看到我们在 Landlord 的 service() 方法中仅仅实现了核心的业务代码,其余的关注点功能是根据我们设置的切面自动补全的。
其实跟上面的实际代码是一样,不过这里有点不同的话,你可以直接用@ComponentScan注解能扫描指定路径下的标识了Spring Bean注解(@Component或者是@Component参与合成的注解。
下面就再次啰嗦一下分点说明如何使用注解实现AOP
Spring 是方法级别的 AOP 框架,我们主要也是以某个类额某个方法作为连接点,另一种说法就是:选择哪一个类的哪一方法用以增强功能。
.... public void service() { // 仅仅只是实现了核心的业务功能 System.out.println("签合同"); System.out.println("收房租"); } ....
我们在这里就选择上述 Landlord 类中的 service() 方法作为连接点。
选择好了连接点就可以创建切面了,我们可以把切面理解为一个拦截器,当程序运行到连接点的时候,被拦截下来,在开头加入了初始化的方法,在结尾也加入了销毁的方法而已,在 Spring 中只要使用 @Aspect 注解一个类,那么 Spring IoC 容器就会认为这是一个切面了:
package aspect;
import org.aspectj.lang.annotation.After;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.springframework.stereotype.Component;
@Component@Aspectclass Broker {
@Before("execution(* pojo.Landlord.service())") public void before(){ System.out.println("带租客看房"); System.out.println("谈价格"); }
@After("execution(* pojo.Landlord.service())") public void after(){ System.out.println("交钥匙"); }}
注意:被定义为切面的类仍然是一个 Bean ,需要 @Component 注解标注,注入到Spring容器中。
代码部分中在方法上面的注解看名字也能猜出个大概,下面来列举一下 Spring 中的 AspectJ 注解:
注解 | 说明 |
---|---|
@Before | 前置通知,在连接点方法前调用 |
@Around | 环绕通知,它将覆盖原有方法,但是允许你通过反射调用原有方法,后面会讲 |
@After | 后置通知,在连接点方法后调用 |
@AfterReturning | 返回通知,在连接点方法执行并正常返回后调用,要求连接点方法在执行过程中没有发生异常 |
@AfterThrowing | 异常通知,当连接点方法异常时调用 |
在上面的注解中定义了 execution 的正则表达式,Spring 通过这个正则表达式判断具体要拦截的是哪一个类的哪一个方法:
execution(* pojo.Landlord.service())
依次对这个表达式作出分析:
execution:代表执行方法的时候会触发 * :代表任意返回类型的方法 pojo.Landlord:代表类的全限定名 service():被拦截的方法名称
通过上面的表达式,Spring 就会知道应该拦截 pojo.Lnadlord 类下的 service() 方法。上面的演示类还好,如果多出都需要写这样的表达式难免会有些复杂,我们可以通过使用 @Pointcut 注解来定义一个切点来避免这样的麻烦:
package aspect;
import org.aspectj.lang.annotation.After;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;
@Component@Aspectclass Broker {
@Pointcut("execution(* pojo.Landlord.service())") public void lService() { }
@Before("lService()") public void before() { System.out.println("带租客看房"); System.out.println("谈价格"); }
@After("lService()") public void after() { System.out.println("交钥匙"); }}
我们来探讨一下环绕通知,这是 Spring AOP 中最强大的通知,因为它集成了前置通知和后置通知,它保留了连接点原有的方法的功能,所以它及强大又灵活,让我们来看看:
package aspect;
import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.springframework.stereotype.Component;
@Component@Aspectclass Broker {
// 注释掉之前的 @Before 和 @After 注解以及对应的方法// @Before("execution(* pojo.Landlord.service())")// public void before() {// System.out.println("带租客看房");// System.out.println("谈价格");// }//// @After("execution(* pojo.Landlord.service())")// public void after() {// System.out.println("交钥匙");// }
// 使用 @Around 注解来同时完成前置和后置通知 @Around("execution(* pojo.Landlord.service())") public void around(ProceedingJoinPoint joinPoint) { System.out.println("带租客看房"); System.out.println("谈价格");
try { joinPoint.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); }
System.out.println("交钥匙"); }}
运行测试代码,结果仍然正确:
使用 XML 配置
注解是很强大的东西,但基于 XML 的开发我们仍然需要了解,我们先来了解一下 AOP 中可以配置的元素:
AOP 配置元素 | 用途 | 备注 |
---|---|---|
aop:advisor | 定义 AOP 的通知其 | 一种很古老的方式,很很少使用 |
aop:aspect | 定义一个切面 | —— |
aop:before | 定义前置通知 | —— |
aop:after | 定义后置通知 | —— |
aop:around | 定义环绕通知 | —— |
aop:after-returning | 定义返回通知 | —— |
aop:after-throwing | 定义异常通知 | —— |
aop:config | 顶层的 AOP 配置元素 | AOP 的配置是以它为开始的 |
aop:declare-parents | 给通知引入新的额外接口,增强功能 | —— |
aop:pointcut | 定义切点 | —— |
有了之前通过注解来编写的经验,并且有了上面的表,我们将上面的例子改写成 XML 配置很容易(去掉所有的注解):
<!-- 装配 Bean--><bean name="landlord" class="pojo.Landlord"/><bean id="broker" class="aspect.Broker"/>
<!-- 配置AOP --><aop:config> <!-- where:在哪些地方(包.类.方法)做增加 --> <aop:pointcut id="landlordPoint" expression="execution(* pojo.Landlord.service())"/> <!-- what:做什么增强 --> <aop:aspect id="logAspect" ref="broker"> <!-- when:在什么时机(方法前/后/前后) --> <aop:around pointcut-ref="landlordPoint" method="around"/> </aop:aspect></aop:config>
运行测试程序,看到正确结果:
参考文章
https://www.cnblogs.com/xuyatao/p/8485851.html https://www.cnblogs.com/wmyskxz/p/8835243.html