JavaWeb 之 Spring AOP 面向切面编程

Spring AOP 面向切面编程


Spring 框架核心功能之 AOP 技术


AOP 的概述

什么是 AOP 的技术?

  • 在软件业,AOP 为 Aspect Oriented Programming 的缩写,意为:面向切面编程
  • AOP 是一种编程范式,隶属于软工范畴,指导开发者如何组织程序结构。
  • AOP 最早由 AOP 联盟的组织提出的,制定了一套规范。Spring 将 AOP 思想引入到框架中,必须遵守 AOP 联盟的规范。
  • 通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。
  • AOP 是 OOP 的延续,是软件开发中的一个热点,也是 Spring 框架中的一个重要内容,是函数式编程的一种衍生范型。
  • 利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
  1. AOP:面向切面编程。(思想————解决 OOP 遇到一些问题)
  2. AOP 采取横向抽取机制,取代了传统纵向继承体系重复性代码(性能监视、事务管理、安全检查、缓存)

为什么要学习 AOP

  • 可以在不修改源代码的前提下,对程序进行增强!

Spring 框架的 AOP 的底层实现

代理方式

Srping 框架的 AOP 技术底层也是采用的代理技术,代理的方式提供了两种

  • 基于 JDK 的动态代理

    必须是面向接口的,只有实现了具体接口的类才能生成代理对象

  • 基于 CGLIB 动态代理

    对于没有实现了接口的类,也可以产生代理,产生这个类的子类的方式

Spring 的传统 AOP 中根据类是否实现接口,来采用不同的代理方式

  • 如果实现类接口,使用 JDK 动态代理完成 AOP

  • 如果没有实现接口,采用 CGLIB 动态代理完成 AOP

JDK 的动态代理(代码了解,理解原理)

使用 Proxy 类来生成代理对象的一些代码如下:

注意:得有接口才能使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 使用 JDK 的方式生成代理对象
*/
public class MyProxyUtils {
public static UserDao getProcy(UserDao dao) {
// 使用 Proxy 生成代理对象
UserDao proxy = (UserDao) Proxy.newProxyInstance(dao.getClass().getClassLoader(), dao.getClass().getInterfaces(), new InvocationHandler() {
// 代理对象方法一执行,invoke 方法就会执行一次
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 代理增强方法
if("save".equals(method.getName())){
System.out.println("记录日志…");
// 开启事务
}
// 提交事务
// 让 dao 类的 save 或者 update 方法正常的执行下去
return method.invoke(dao, args);
}
});
// 返回代理对象
return proxy;
}
}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class demo1 {
@Test
public void run1() {
UserDao dao = new UserDaoImpl();
// 原始的调用方式
dao.save();
dao.update();
System.out.println("==================");
// 使用工具类,获取到代理对象
UserDao procy = MyProxyUtils.getProcy(dao);
// 调用代理对象的方法
procy.save();
procy.update();
}
}

CGLIB 的代理技术(了解)

  • 引入 CBLIB 的开发包

    如果想使用 CGLIB 的技术来生成代理对象,那么需要引入 CGLIB 的开发的 jar 包,在 Spring 框架核心包中已经引入了 CGLIB 的开发包了。所以直接引入 Spring 核心开发包即可!

编写相关的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class MyCglibUtils {
/**
* 使用 CGLIB 方式生成代理对象
*
* @return
*/
public static BookDaoImpl getProxy() {
Enhancer enhancer = new Enhancer();
// 设置父类
enhancer.setSuperclass(BookDaoImpl.class);
// 设置回调函数
enhancer.setCallback(new MethodInterceptor() {
// 代理对象的方法执行,回调函数就会执行
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
if (method.getName().equals("save")) {
System.out.println("记录日志…");
}
return methodProxy.invokeSuper(o, objects);
}
});
// 生成代理对象
BookDaoImpl proxy = (BookDaoImpl) enhancer.create();
return proxy;
}
}

测试:

1
2
3
4
5
6
7
@Test
public void run1() {
// 使用 CGLIB 生成代理对象
BookDaoImpl proxy = MyCglibUtils.getProxy();
proxy.save();
proxy.update();
}

Spring 基于 AspectJ 的 AOP 的开发


AOP 的相关术语

  1. Joinpoint(连接点)————所谓连接点是指那些被拦截到的点。在 spring 中,这些点指的是方法,因为 spring 只支持方法类型的连接点
  2. Pointcut(切入点)————所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义
  3. Advice(通知/增强)————所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知.通知分为前置通知,后置通知,异常通知,最终通知,环绕通知(切面要完成的功能)
  4. Introduction(引介)————引介是一种特殊的通知在不修改类代码的前提下, Introduction 可以在运行期为类动态地添加一些方法或 Field
  5. Target(目标对象)————代理的目标对象
  6. Weaving(织入)————是指把增强应用到目标对象来创建新的代理对象的过程
  7. Proxy(代理)————一个类被 AOP 织入增强后,就产生一个结果代理类
  8. Aspect(切面)————是切入点和通知的结合,需要自己来编写和配置的

具体点就是:

连接点:UserDaoImpl 中的所有方法都可以称为连接点。
切入点:拦截哪些方法(对哪些方法进行增强)。
通知/增强:具体做什么功能,比如记录日志。
目标对象:UserDaoImpl 称为目标对象。
织入:把增强添加到目标对象,生成代理对象的过程。
代理:生成的代理对象。
切面:切入点 + 通知,组合称为切面。通知需要自己来编写,切入点需要配置。

AspectJ 的 XML 方式完成 AOP 开发

第一个案例

步骤一:创建 JavaWEB 项目,引入具体的开发的 jar 包

  • 先引入 Spring 框架开发的基本开发包(6个)

  • 再引入 Spring 框架的 AOP 的开发包(4个)

    • Spring 的传统 AOP 的开发的包

      spring-aop-4.2.4.RELEASE.jar
      com.springsource.org.aopalliance-1.0.0.jar

    • aspectJ 的开发包

      com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
      spring-aspects-4.2.4.RELEASE.jar

步骤二:创建 Spring 的配置文件,引入具体的 AOP 的 schema 约束

1
2
3
4
5
6
<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>

步骤三:创建包结构,编写具体的接口和实现类

  • com.renkaigis.demo4

    CustomerDao – 接口
    CustomerDaoImpl – 实现类

步骤四:将目标类配置到 Spring 中

1
2
<!--配置客户的 dao-->
<bean id="customerDao" class="com.renkaigis.demo4.CustomerDaoImpl"/>

步骤五:定义切面类

1
2
3
4
5
6
7
8
9
10
11
/**
* 切面类:切入点 + 通知
*/
public class MyAspectXml {
/**
* 通知(具体的增强)
*/
public void log(){
System.out.println("记录日志…");
}
}

步骤六:在配置文件中定义切面类

1
2
<!--配置切面类-->
<bean id="myAspectXml" class="com.renkaigis.demo4.MyAspectXml"/>

步骤七:在配置文件中完成aop的配置

1
2
3
4
5
6
7
8
9
<!--配置 AOP-->
<aop:config>
<!--引入切面类,配置切面类的切入点 + 通知(类型)-->
<aop:aspect ref="myAspectXml">
<!--配置前置通知,save 方法执行之前,增强的方法会执行-->
<!--定义通知类型:切面类的方法和切入点的表达式 execution(public void com.renkaigis.demo4.CustomerDaoImpl.save()) -->
<aop:before method="log" pointcut="execution(public void com.renkaigis.demo4.CustomerDaoImpl.save())"/>
</aop:aspect>
</aop:config>

完成测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 测试 AOP 功能
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class Demo4 {
@Resource(name = "customerDao")
private CustomerDao customerDao;

@Test
public void run1(){
customerDao.save();
customerDao.update();
}
}

切入点的表达式

在配置切入点的时候,需要定义表达式,重点的格式如下:execution(public * *(..)),具体展开如下:

  • 切入点表达式的格式如下:

    execution([修饰符] 返回值类型 包名.类名.方法名(参数))

  • 修饰符可以省略不写,不是必须要出现的。

  • 返回值类型是不能省略不写的,根据你的方法来编写返回值。可以使用 * 代替。

  • 包名例如:com.renkaigis.demo4.BookDaoImpl

    首先 com 是不能省略不写的,但是可以使用 * 代替
    中间的包名可以使用 * 号代替
    如果想省略中间的包名可以使用 *..*

  • 类名也可以使用 * 号代替,也有类似的写法:*DaoImpl

  • 方法也可以使用 * 号代替,save*()

  • 参数如果是一个参数可以使用 * 号代替,如果想代表任意参数使用 ..

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<aop:aspect ref="myAspectXml">
<!--切入点的表达式:-->
<!--1. execution() 固定的,不能不写-->
<aop:before method="log" pointcut="execution(public void com.renkaigis.demo4.CustomerDaoImpl.save())"/>
<!--2. pulbic 可以省略不写-->
<aop:before method="log" pointcut="execution(void com.renkaigis.demo4.CustomerDaoImpl.save())"/>
<!--3. void,返回值写 * 表示任意的返回值,返回值类型不能不写-->
<aop:before method="log" pointcut="execution(* com.renkaigis.demo4.CustomerDaoImpl.save())"/>
<!--4. 包名可以使用 * 来代替,com.renkaigis.*,简写 *..*,不能不写-->
<aop:before method="log" pointcut="execution(* *..*.CustomerDaoImpl.save())"/>
<!--5. 类名也可以使用 * 号代替,也有类似的写法:*DaoIml-->
<aop:before method="log" pointcut="execution(* *..*.*DaoImpl.save())"/>
<!--6. 方法也可以使用 * 号代替,save*()-->
<aop:before method="log" pointcut="execution(* *..*.*DaoImpl.save*())"/>
<!--7. 方法的参数:可以使用 * 号代替,如果想代表任意参数使用 .. -->
<aop:before method="log" pointcut="execution(* *..*.*DaoImpl.save*(..))"/>
</aop:aspect>

AOP的通知类型

  1. 前置通知

    在目标类的方法执行之前执行。
    配置文件信息:<aop:after method="before" pointcut-ref="myPointcut3"/>
    应用:可以对方法的参数来做校验

  2. 最终通知

    在目标类的方法执行之后执行,如果程序出现了异常,最终通知也会执行。
    配置文件信息:<aop:after method="after" pointcut-ref="myPointcut3"/>
    应用:例如像释放资源

  3. 后置通知

    方法正常执行后的通知。出现异常,不会执行。
    配置文件信息:<aop:after-returning method="afterReturning" pointcut-ref="myPointcut2"/>
    应用:可以修改方法的返回值

  4. 异常抛出通知

    在抛出异常后通知
    配置文件信息:<aop:after-throwing method="afterThorwing" pointcut-ref="myPointcut3"/>
    应用:包装异常的信息

  5. 环绕通知

    方法的执行前后执行。
    配置文件信息:<aop:around method="around" pointcut-ref="myPointcut2"/>
    要注意:目标的方法默认不执行,需要使用 ProceedingJoinPoint 对来让目标对象的方法执行。

1
2
3
4
5
6
7
8
9
10
/**
* 环绕通知:方法执行之前和方法执行之后进行通知
* 默认的情况下,目标对象的方法不能执行,需要手动让目标对象的方法执行。
*/
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("环绕通知1…");
// 手动让目标对象的方法执行
joinPoint.proceed();
System.out.println("环绕通知2…");
}

Spring框架的AOP技术之注解方式

第一个案例

步骤一:创建 JavaWEB 项目,引入具体的开发的 jar 包

同上。

步骤二:创建 Spring 的配置文件,引入具体的 AOP 的 schema 约束

同上。

这里我引入一个最全的约束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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"
xmlns:tx="http://www.springframework.org/schema/tx"
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
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
</beans>

步骤三:创建包结构,编写具体的接口和实现类

  • com.renkaigis.demo5

    CustomerDao————接口
    CustomerDaoImpl————实现类

步骤四:将目标类配置到 Spring 中

1
2
<!--配置客户的 dao-->
<bean id="customerDao" class="com.renkaigis.demo4.CustomerDaoImpl"/>

步骤四:将目标类配置到 Spring 中

1
2
<!--配置目标对象-->
<bean id="customerDao" class="com.renkaigis.demo5.CustomerDaoImpl"/>

步骤五:定义切面类

添加切面和通知的注解
  • @Aspect————定义切面类的注解

  • 通知类型(注解的参数是切入点的表达式

    @Before————前置通知
    @AfterReturing————后置通知
    @Around————环绕通知
    @After————最终通知
    @AfterThrowing————异常抛出通知

具体的代码如下
1
2
3
4
5
6
7
8
9
10
/**
* 注解方式的切面类
*/
@Aspect
public class MyAspectAnno {
@Before(value = "execution(public * com.renkaigis.demo5.CustomerDaoImpl.save())")
public void log() {
System.out.println("记录日志…");
}
}

步骤六:在配置文件中定义切面类

1
2
<!--配置切面类-->
<bean id="myAspectAnno" class="com.renkaigis.demo5.MyAspectAnno"/>

步骤七:在配置文件中开启自动代理

1
2
<!--开启自动代理-->
<aop:aspectj-autoproxy/>

步骤八:完成测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* AOP 注解方式
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class Demo5 {
@Resource(name = "customerDao")
private CustomerDao customerDao;

@Test
public void run1(){
customerDao.save();
customerDao.update();
}
}

通知类型

通知类型

  • @Before————前置通知
  • @AfterReturing————后置通知
  • @Around————环绕通知(目标对象方法默认不执行的,需要手动执行)
  • @After————最终通知
  • @AfterThrowing————异常抛出通知

配置通用的切入点

  • 使用 @Pointcut 定义通用的切入点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Aspect
public class MyAspectAnno {
// 引入自定义切入点
@Before(value = "MyAspectAnno.fn()")
public void log() {
System.out.println("记录日志…");
}

/**
* 自定义切入点,@Pointcut
*/
@Pointcut(value = "execution(public * com.renkaigis.demo5.CustomerDaoImpl.save())")
public void fn() {
}
}