β

Spring AOP笔记

漠然 Blog 460 阅读

Spring-AOP-logo

一、AOP 简介

AOP(Aspect-Oriented Programming, 面向切面编程): 是一种新的方法论, 是对传统 OOP(Object-Oriented Programming,

面向对象编程) 的补充.AOP 的主要编程对象是切面(aspect), 而切面模块化横切关注点.在应用 AOP 编程时, 仍然需要定

义公共功能, 但可以明确的定义这个功能在哪里, 以什么方式应用, 并且不必修改受影响的类. 这样一来横切关注点就被

<模块化到特殊的对象(切面)里.

二、Hello World

1、项目结构(项目采用Maven构建,实际原因是我特么没找到Spring完整jar)

Spring-Aop-HelloWorld

2、POM.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>Spring4</groupId>
	<artifactId>Spring4</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<build>
		<sourceDirectory>src</sourceDirectory>
		<plugins>
			<plugin>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.1</version>
				<configuration>
					<source>1.7</source>
					<target>1.7</target>
				</configuration>
			</plugin>
		</plugins>
	</build>
	<dependencies>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context</artifactId>
			<version>4.1.7.RELEASE</version>
		</dependency>
		<dependency>
			<groupId>org.aspectj</groupId>
			<artifactId>aspectjweaver</artifactId>
			<version>1.8.6</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-aspects</artifactId>
			<version>4.1.7.RELEASE</version>
		</dependency>
	</dependencies>
</project>

3、applicationContext.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:context="http://www.springframework.org/schema/context"
	xmlns:util="http://www.springframework.org/schema/util"
	xmlns:p="http://www.springframework.org/schema/p"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.1.xsd
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.1.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd">
	<context:component-scan base-package="me.mritd"></context:component-scan>
	<aop:aspectj-autoproxy></aop:aspectj-autoproxy>

</beans>

4、Aop及测试类

TestController

package me.mritd.mvc.controller;

import org.springframework.stereotype.Controller;

@Controller
public class TestController {
	
	public void test1(){
		System.out.println("test1........");
	}
}

TestAop1

package me.mritd.test;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class TestAop1 {
	
	@Before("execution(* me.mritd.mvc.controller.TestController.*(..) )")
	public void testAop(){
		System.out.println("Aop....");
	}
}

测试代码

@Test
public void test() {
	ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
	TestController controller = (TestController) ctx.getBean("testController");
	controller.test1();
}

三、Spring Aop 详解

Spring AOP 采用动态代理的方式动态生成代理类,以实现Aop切面编程;动态代理功能来源于JDK原生

支持和AspectJ;AspectJ是一个成熟的Aop 框架,Spring将其集成了进来。

AOP编程的一些术语定义:

★ 连接点(Joinpoint)

程序执行的某个特定位置:如类开始初始化前、类初始化后、类某个方法调用前、调用后、方法

抛出异常后。这些代码中的特定点,称为“连接点”。Spring仅支持方法的连接点,即仅能在方法

调用前、方法调用后、方法抛出异常时以及方法调用前后这些程序执行点织入增强。

★ 切点(Pointcut)

每个程序类都拥有多个连接点,如一个拥有两个方法的类,这两个方法都是连接点。但在这为数

众多的连接点中,如何定位到某个感兴趣的连接点上呢?AOP通过“切点”定位特定的连接点。通过

数据库查询的概念来理解切点和连接点的关系:连接点相当于数据库中的记录,而切点相当于查询

条件。一个切点可以匹配多个连接点。Spring中,切点通过Pointcut接口进行描述

★ 增强(Advice)

增强是织入到目标类连接点上的一段程序代码。增强既包含了用于添加到目标连接点上的一段执行

逻辑,又包含了用于定位连接点的方为信息,所以Spring所提供的增强接口都是带方位名的:

BeforeAdvice(方法调用前的位置)、AfterReturningAdvice(访问返回后的位置)、ThrowsAdvice等。

★ 目标对象(Target)

增强逻辑的织入目标类。

★ 织入(Weaving)

织入是将增强添加到目标类具体连接点上的过程。AOP有三种织入方式:

1)编译期织入,这要求使用特殊的Java编译器;

2)类装载期织入,这要求使用特殊的类装载器;

3)动态代理织入,在运行期为目标类添加增强生成子类的方式。

Spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。

★ 切面(Aspect)

切面由切点和增强组成,它既包括了横切逻辑的定义,也包括了连接点的定义,Spring AOP就是负责实施

切面的框架,它将切面所定义的横切逻辑织入到切面所指定的连接点中。

★ AOP的工作重心,主要包括两个:

1)如何通过切点和增强定位到连接点上;

2)如何在增强中编写切面的代码。

★ 其他基础知识

Spring AOP使用了两种代理机制:一种是基于JDK的动态代理,另一种是基于CGLib的动态代理。之所以需要

两种代理机制,很大程度上是因为JDK本身只提供接口的代理,而不支持类的代理。

JDK的动态代理主要涉及到java.lang.reflect包中的两个类:Proxy和InvocationHandler。

CGLib采用非常底层的字节码技术,可以为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用。

有研究表明,CGLib所创建的动态代理对象的性能比JDK的所创建的代理对象的性能高了不少。但CGLib在创建代理对象

时所花费的时间却比JDK动态代理多,所以对于singleton的代理对象或者具有实例池的代理,因为无须频繁创建代理对象,

所以比较适合用CGLib动态代理技术,反之则适合用JDK动态代理技术。  CGLib不能对目标类中的final方法进行代理。

1、在Spring中启用Aop注解

要使用Aop,在Spring中可以使用注解方式和XML配置方式,要在 Spring 应用中使用 AspectJ 注解:

★ 必须在 classpath 下包含 AspectJ 类库: aopalliance.jar、aspectj.weaver.jar 和 spring-aspects.jar

★ 将 aop Schema 添加到 <beans> 根元素中.

★ 要在 Spring IOC 容器中启用 AspectJ 注解支持, 只要在 Bean 配置文件中定义一个空的 XML

元素 <aop:aspectj-autoproxy>

★ 当 Spring IOC 容器侦测到 Bean 配置文件中的 <aop:aspectj-autoproxy> 元素时, 会自动为与

AspectJ 切面匹配的 Bean 创建代理.

2、使用AspectJ注解声明切面

要在 Spring 中声明 AspectJ 切面, 只需要在 IOC 容器中将切面声明为 Bean 实例. 当在 Spring IOC

容器中初始化 AspectJ 切面之后, Spring IOC 容器就会为那些与 AspectJ 切面相匹配的 Bean 创建代理.

在 AspectJ 注解中, 切面只是一个带有 @Aspect 注解的 Java 类.

通知是标注有某种注解的简单的 Java 方法,AspectJ 支持 5 种类型的通知注解:

◆ @Before: 前置通知, 在方法执行之前执行

◆ @After: 后置通知, 在方法执行之后执行

◆ @AfterRunning: 返回通知, 在方法返回结果之后执行

◆ @AfterThrowing: 异常通知, 在方法抛出异常之后

◆ @Around: 环绕通知, 围绕着方法执行

〓 前置通知 @Before 在方法执行前执行;我们可以通过在通知中使用 JoinPoint 参数获取方法执行时的细节,包括传入的参数、执行方法名等

UserDao

package me.mritd.dao;

import org.springframework.stereotype.Repository;

@Repository
public class UserDao {
	public void saveUser(String userName,String userID){
		System.out.println("执行保存操作......");
	}
}

UserAspect

package me.mritd.aspect;

import java.util.Arrays;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class UserAspect {
	@Before("execution(* me.mritd.dao.UserDao.saveUser(..))")
	public void testBefore(JoinPoint joinPoint){
		System.out.println("执行的方法名 :"+joinPoint.getSignature().getName());
		System.out.println("执行参数 :"+Arrays.asList(joinPoint.getArgs()));
	}
}

测试代码

@Test
public void testBefore(){
	ClassPathXmlApplicationContext cts = new ClassPathXmlApplicationContext("applicationContext.xml");
	UserDao dao = (UserDao) cts.getBean("userDao");
	dao.saveUser("zhangsan", "9527");
}

控制台输出

信息: Loading XML bean definitions from class path resource [applicationContext.xml]
执行的方法名 :saveUser
执行参数 :[zhangsan, 9527]
执行保存操作......

〓 后置通知 @After 无论方法是否产生一场都会继续执行,所以说后置通知记录了方法的结束为止(可能异常终止)

UserDao

public void saveUser(String userName,String userID){
    System.out.println("执行保存操作......");
    System.out.println(1/0);
}

UserAspect

@After("execution(* me.mritd.dao.UserDao.saveUser(..))")
public void testAfter(){
    System.out.println("方法运行结束......");
}

测试代码同上,当出现异常后 @After 通知仍会执行

〓 返回通知  @AfterRunning 和后置通知 @After 类似,但返回通知可以拿到方法执行完成后的返回值,

且若方法出现异常后返回通知不执行通过在注解中使用 returning 指定 返回值参数名,在方法中传入

Object型(可以是其他的类型,但前提是你知道 代理对象执行方法的返回值;一般切面编程会横切多个方法,

若多个方法返回值不通则需要使用Object类型) 返回值即可 来获取返回值

UserDao

public boolean saveUser(String userName,String userID){
	System.out.println("执行保存操作......");
	return true;
}

UserAspect

@AfterReturning(pointcut="execution(* me.mritd.dao.UserDao.saveUser(..))",returning="result")
public void testAfterReturning(JoinPoint joinPoint,Object result){
	System.out.println("方法细节 :"+joinPoint.getSignature().getName());
	System.out.println("返回值 :"+result);
}

测试略过…..

〓 异常通知 @AfterThrowing 可以在当程序出现异常后执行,并能访问程序产生的异常信息;同返回通知一样

若想访问其中异常信息,必须使用 throwing指定返回的异常放入那个参数

UserDao 同样打印1/0 来产生异常,UserAspect 如下

@AfterThrowing(pointcut="execution(* me.mritd.dao.UserDao.saveUser(..))",throwing="exception")
//在此传入的异常类型即为要捕捉的异常,即只有出现此类异常 异常通知才会执行
public void testThrowing(Exception exception) {
	System.out.println("异常信息 :"+exception.getMessage());
}

测试代码略过……

〓 环绕通知 @Around 在方法执行前后各执行一次,环绕通知相较于其他通知功能最为强大,但并不常用;

环绕通知必须有一个传入的连接点类型为 ProceedingJoinPoint 的参数,ProceedingJoinPoint . 它是 JoinPoint

的子接口, 允许控制何时执行, 是否执行连接点.在环绕通知中需要明确调用 ProceedingJoinPoint 的 proceed()

方法来执行被代理的方法. 如果忘记这样做就会导致通知被执行了, 但目标方法没有被执行.环绕通知必须有返回值

否则会出现空指针异常

UserAspect

@Around("execution(* me.mritd.dao.UserDao.saveUser(..))")
public Object testAround(ProceedingJoinPoint joinPoint) {
	Object result = null;
	try {
		System.out.println("此处执行代码 相当于前置通知");
		result = joinPoint.proceed();
		System.out.println("此处执行代码 相当于后置通知");
	} catch (Throwable e) {
		System.out.println("此处执行代码 相当于异常通知");
		e.printStackTrace();
	}
	System.out.println("此处执行代码 相当于返回通知");
	return result;
}

测试省略……

3、AspectJ 切点表达式

每种通知须定义与其匹配的切点表达式,切点表达式的基本规则语法如下,更深层次请参考AspectJ

◆ execution * com.atguigu.spring.ArithmeticCalculator.*(..):

匹配 ArithmeticCalculator 中声明的所有方法,第一个 * 代表任意修饰符及任意返回值. 第二个 *

代表任意方法. .. 匹配任意数量的参数. 若目标类与接口与该切面在同一个包中, 可以省略包名.

◆ execution public * ArithmeticCalculator.*(..): 匹配 ArithmeticCalculator 接口的所有公有方法.

◆ execution public double ArithmeticCalculator.*(..): 匹配 ArithmeticCalculator 中返回 double 类型数值的方法

◆ execution public double ArithmeticCalculator.*(double, ..):

匹配第一个参数为 double 类型的方法, .. 匹配任意数量任意类型的参数

◆ execution public double ArithmeticCalculator.*(double, double): 匹配参数类型为 double, double 类型的方法.

切点表达式可以使用 判断操作符,即与或非操作

Spring-Aspect-execution

当多个切入点表达式相同时时,可以重用切点表达式;通常通过 @Pointcut 注解将一个切入点声明成简单的方法.

切入点的方法体通常是空的, 因为将切入点定义与应用程序逻辑混在一起是不合理的. 切入点方法的访问控制符同时

也控制着这个切入点的可见性. 如果切入点要在多个切面中共用, 最好将它们集中在一个公共的类中. 在这种情况下,

它们必须被声明为 public. 在引入这个切入点时, 必须将类名也包括在内. 如果类没有与这个切面放在同一个包中,

还必须包含包名.其他通知只要填写这个方法名即可

Spring-Aop-pointcut

4、切面优先级问题

当两个AOP切面中同时对某一方法都能切到时,比如一个日志切面负责打印日志;对某一方法进行前置通知,

而另一个切面负责初始化,同样对这方法进行了前置通知;这时就产生了两个切面都能切到同一个方法的问题

那么此时 如果不人为干预,那么两个切面中的前置通知的执行顺序是不确定的;所以我们可以通过在切面上

使用 @Order 注解手动指定执行顺序,此时 @Order括号内值越小,优先级越高

四、使用XML 配置AOP

配置样例

<!-- 声明一个切面为普通的bean -->
<bean id="userAspect" class="me.mritd.aspect.UserAspect"></bean>
<aop:config>
	<!-- 声明一个切点表达式 -->
	<aop:pointcut expression="execution(* me.mritd.dao.UserDao.saveUser(..))" id="exec"/>
	<!-- 指定使用那一个 切面 -->
	<aop:aspect order="1" ref="userAspect">
		<!-- 标签指定通知类型  method指定使用切面类的那个方法 pointcut-ref指定使用哪个切点表达式-->
		<aop:before method="testBefore" pointcut-ref="exec"/>
	</aop:aspect>
</aop:config>
作者:漠然 Blog
十字路口,繁华街头......
原文地址:Spring AOP笔记, 感谢原作者分享。