Java -- Spring学习笔记5、AOP面向切面编程
1、AspectJ对AOP的实现
AspectJ 是一个优秀面向切面的框架,它扩展了Java语言,提供了强大的切面实现、实现了AOP的功能,且其实现方式更为简捷,使用更为方便,而且还支持注解式开发。所以、在Spring中使用AOP开发时,一般使用AspectJ的实现方式。
2、AspectJ的通知类型
- 常用的有五种类型:
- 前置通知
- 后置通知
- 环绕通知
- 异常通知
- 最终通知
3、AspectJ的切入点表达式
- AspectJ 定义了专门的表达式用于指定切入点。表达式的原型是:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
- 解释:
- modifiers-pattern 访问权限类型
- ret-type-pattern 返回值类型
- declaring-type-pattern 包名类名
- name-pattern(param-pattern) 方法名(参数类型和参数个数)
- throws-pattern 抛出异常类型
- ?表示可选的部分
- 以上表达式共 4 个部分。
execution(访问权限 方法返回值 方法声明(参数) 异常类型)
切入点表达式要匹配的对象就是目标方法的方法名。所以,execution 表达式中明显就是方法的签名。注意,表达式中黑色文字表示可省略部分,各部分间用空格分开。在其中可以使用以下符号:
- *:0至多个任意字符
- ..:用在方法参数中、表示任意参数。用在包名后表示当前包及其子包路径。
- +:用在类名后、表示当前类及其子类。用在接口后、表示当前接口以及实现类。
- 举例:
execution(public * *(..))
指定切入点为:任意公共方法。
execution(* set*(..))
指定切入点为:任何一个以“set”开始的方法。
execution(* com.rg.service.*.*(..))
指定切入点为:定义在 service 包里的任意类的任意方法。
execution(* com.rg.service..*.*(..))
指定切入点为:定义在 service 包或者子包里的任意类的任意方法。“..”出现在类名中时,后面必须跟“*”,表示包、子包下的所有类。
execution(* *..service.*.*(..))
指定所有包下的 serivce 子包下所有类(接口)中所有方法为切入点
........................
4、AspectJ的开发环境
- 添加maven依赖:
<!-- 依赖 -->
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
</dependencies>
<!-- 插件 -->
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
5、AspectJ基于注解的AOP实现
5.1、前置通知@Before -- 目标方法执行之前执行
- 在dao层定义接口和实现类、如下:
//定义接口
public interface SomeService
{
void doSome();
}
//实现类
public class SomeServiceImpl implements SomeService
{
@Override
public void doSome()
{
System.out.println("...........执行了doSome()方法............");
}
}
- 定义切面类、添加前置通知增强方法、如下:
/**
* @Aspect:AspectJ框架的注解、表示当前类是切面类
*/
@Aspect
public class MyAspect
{
/**
* @Before:前置通知、也就是在被切入方法之前执行
* value:表示切入位置、这里的表达式指:任何返回值类型和任何包下的doSome()方法,参数类型和个数也是任意。
* 位置:方法上边
*/
@Before(value = "execution(* *..doSome(..))")
public void before()
{
System.out.println("前置通知、在方法前执行........");
}
}
- 声明目标类对象和切面类对象、如下:
<!--目标类对象、接口实现类、因为在测试方法中需要调用该类中的业务方法-->
<bean id="someService" class="com.rg.dao.impl.SomeServiceImpl"/>
<!--切面类对象-->
<bean id="myAspect" class="com.rg.aspect.MyAspect"/>
- 注册 AspectJ的自动代理、如下:
<!--
定义好切面Aspect后,需要通知Spring容器,让容器生成“目标类+切面”的代理
对象。代理是由容器自动生成的。只需要在 Spring 配置文件中注册一个基于 aspectj 的
自动代理生成器,其就会自动扫描到@Aspect注解,并按通知类型与切入点,将其织入,并
生成代理。
其工作原理是,<aop:aspectj-autoproxy/>通过扫描找到@Aspect 定义的切面类,再由切
面类根据切入点找到目标类的目标方法,再由通知类型找到切入的时间点。
-->
<aop:aspectj-autoproxy/>
- 测试方法:
public class MyTest
{
@Test
public void test01()
{
//指定spring配置文件
String config = "applicationContext.xml";
//创建spring容器对象
ApplicationContext ctx = new ClassPathXmlApplicationContext(config);
//从spring容器中获取对象、使用id
SomeService someService = (SomeService) ctx.getBean("someService");
//执行对象的业务方法
someService.doSome();
}
}
- 控制台输出:
前置通知、在方法前执行........
...........执行了doSome()方法............
5.2、JoinPoint参数
所有的通知方法均可包含一个JoinPoint类型的参数、该类型的对象本身就是切入点表达式。通过该参数,可获取切入点表达式、方法签名、目标对象等。
- 对上边接口和测试方法稍作修改:
//添加两个参数
void doSome(String name,int age);
- 切面方法中通过JoinPoint获取参数信息:
/**
* @Before:前置通知、也就是在被切入方法之前执行 value:表示切入位置、这里的表达式指:任何返回值类型和任何包下的doSome()方法,参数类型和个数也是任意。
* 位置:方法上边
*/
@Before(value = "execution(* *..doSome(..))")
public void before(JoinPoint jp)
{
System.out.println("被织入方法签名:" + jp.getSignature());
System.out.println("被织入方法中的参数数量:" + jp.getArgs().length);
Object argList[] = jp.getArgs();
for (Object arg : argList)
{
System.out.println(arg);
}
System.out.println("前置通知、在方法前执行........");
}
- 测试方法调用业务方法:
@Test
public void test01()
{
//指定spring配置文件
String config = "applicationContext.xml";
//创建spring容器对象
ApplicationContext ctx = new ClassPathXmlApplicationContext(config);
//从spring容器中获取对象、使用id
SomeService someService = (SomeService) ctx.getBean("someService");
//执行对象的业务方法
someService.doSome("Jack",18);
}
- 控制台输出:
被织入方法签名:void com.rg.dao.SomeService.doSome(String,int)
被织入方法中的参数数量:2
Jack
18
前置通知、在方法前执行........
...........执行了doSome()方法............
5.3、后置通知@AfterReturning -- 目标方法执行之后执行
因为在方法后执行,所以可以获取方法的返回值信息、该注解有一个returning属性、就是用于指定接收方法返回值的变量名的、也就是说、在增强方法的参数列表中、除了可以包含JoinPoint参数外,还可以包含用于接收返回值的变量。该变量最好为Object类型,因为目标方法的返回值可能是任何类型。
- 在接口中添加方法、如下:
public interface SomeService
{
void doSome(String name,int age);
String getName();
}
//实现类中实现该方法
@Override
public String getName()
{
System.out.println("...........执行了getName()方法............");
return "大卫";
}
- 在切面类中添加后置通知增强方法、如下:
/**
* 后置通知
* @param name:目标方法返回值
*/
@AfterReturning(value = "execution(* *..getName(..))", returning = "name")
public void after(Object name)
{
System.out.println("后置通知、在目标方法执行后执行、目标方法返回值是:" + name);
}
- 测试方法:
@Test
public void test01()
{
//指定spring配置文件
String config = "applicationContext.xml";
//创建spring容器对象
ApplicationContext ctx = new ClassPathXmlApplicationContext(config);
//从spring容器中获取对象、使用id
SomeService someService = (SomeService) ctx.getBean("someService");
//执行对象的业务方法
someService.getName();
}
- 控制台输出:
...........执行了getName()方法............
后置通知、在目标方法执行后执行、目标方法返回值是:大卫
5.4、环绕通知@Around -- 目标方法执行之前之后执行
环绕通知、就是将目标方法拿到切面方法中、在增强方法中获取到的目标方法上边和下边加上需要的内容。方法有一个 ProceedingJoinPoint类型的参数、调用proceed()方法,也就是执行目标方法、若目标方法有返回值,则该方法的返回值就是目标方法的返回值。
- 在接口中添加方法、如下:
public interface SomeService
{
void doSome(String name,int age);
String getName();
String getAddress();
}
@Override
public String getAddress()
{
System.out.println("...........执行了getAddress()方法............");
return "中国上海";
}
- 在切面类中添加环绕通知增强方法:
@Around(value = "execution(* *..getAddress(..))")
public Object around(ProceedingJoinPoint pj) throws Throwable
{
Object res = null;
System.out.println("前置功能.....");
//proceed():目标方法、并接收目标方法的返回值
res = pj.proceed();
System.out.println("后置功能.....");
return res;
}
- 测试方法:
@Test
public void test01()
{
//指定spring配置文件
String config = "applicationContext.xml";
//创建spring容器对象
ApplicationContext ctx = new ClassPathXmlApplicationContext(config);
//从spring容器中获取对象、使用id
SomeService someService = (SomeService) ctx.getBean("someService");
//执行对象的业务方法
Object res = someService.getAddress();
System.out.println(res);
}
- 控制台输出:
前置功能.....
...........执行了getAddress()方法............
后置功能.....
中国上海
5.5、@Pointcut定义切入点
如果一个execution切入点表达式可以被多个通知增强方法使用、就可以使用AspectJ提供@Pointcut注解,用于定义execution 切入点表达式、实现代码复用。
其用法是、将@Pointcut注解在一个方法之上,那么所有的 execution的value属性值均可使用该方法名作为切入点。这个使用@Pointcut注解的方法一般使用private,里边也不写代码、没用实际作用。
- 对环绕通知的切面类中代码稍作修改、如下:
/**
* myCut():下边的方法名就是myCut()
* @param pj
* @return
* @throws Throwable
*/
@Around(value = "myCut()")
public Object around(ProceedingJoinPoint pj) throws Throwable
{
Object res = null;
System.out.println("前置功能.....");
//proceed():目标方法、并接收目标方法的返回值
res = pj.proceed();
System.out.println("后置功能.....");
return res;
}
@Pointcut(value = "execution(* *..getAddress(..))")
private void myCut()
{
//不写代码
}
- 测试方法同上、输出结果、如下:
前置功能.....
...........执行了getAddress()方法............
后置功能.....
中国上海
当然、不光有前置、后置、环绕、还有异常通知、最终通知,但是都不常用,了解就行。