《Spring in action 4》(四)面向切面的Spring
面向切面的Spring
Aop 的概念
Aop :Aspect oriented Programming 面向切面编程,面向切面编程是面向对象编程的补充,而不是替代品。在运行时,动态地将代码切入到类的指定方法,指定位置上的编程思想就是面向切面编程。
Aop中的术语
通知(Advice)
通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。它应该应用在某个方法被调用之前?之后?之前或是之后都调用?还是只在方法抛出异常时调用?
Spring切面可以应用的切面有五种:
- 前置通知(Before):在目标方法被调用之前调用通知方法。
- 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么。
- 返回通知(After-Returning):在目标方法成功执行之后调用通知。
- 异常通知(After-Throwing):在目标方法抛出异常之后调用通知。
- 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。
切面(Aspect)
Aspect声明类似与Java中类的声明,在Aspect中包含着一些Pointcut以及相应的 Advice。
连接点(Joint Point)
表示在程序中明确定义的点,典型的包括方法的调用,属性的修改,对类成员的访问以及异常处理程序块的执行等。它自身还可以嵌套其他的Joint Point。
切点(PointCut)
表示一组符合要求的Joint Point, 这些Joint Point 或是通过逻辑关系组合起来,或是通过通配,正则表达式等方法集中起来,它定义了相应的Advice将要发生的地方。
目标对象(Target)
织入Advice的目标对象。
织入(Weaving)
将Apsect和其他对象连接起来,并创建Adviced Object的过程。
案例解释术语
看到上面的术语其实非常的头痛,不知所云,那么下面用一个比较容易理解的例子来说明上述概念:
(摘自网上 https://blog.csdn.net/q982151756/article/details/80513340)
**下面我以一个简单的例子来比喻一下 AOP 中 Aspect, Joint point, Pointcut 与 Advice之间的关系. **
让我们来假设一下, 从前有一个叫爪哇的小县城, 在一个月黑风高的晚上, 这个县城中发生了命案. 作案的凶手十分狡猾, 现场没有留下什么有价值的线索. 不过万幸的是, 刚从隔壁回来的老王恰好在这时候无意中发现了凶手行凶的过程, 但是由于天色已晚, 加上凶手蒙着面, 老王并没有看清凶手的面目, 只知道凶手是个男性, 身高约七尺五寸. 爪哇县的县令根据老王的描述, 对守门的士兵下命令说: 凡是发现有身高七尺五寸的男性, 都要抓过来审问. 士兵当然不敢违背县令的命令, 只好把进出城的所有符合条件的人都抓了起来.
**来让我们看一下上面的一个小故事和 AOP 到底有什么对应关系. **
首先我们知道, 在 Spring AOP 中 Joint point 指代的是所有方法的执行点, 而 point cut 是一个描述信息, 它修饰的是 Joint point, 通过 point cut, 我们就可以确定哪些 Joint point 可以被织入 Advice. 对应到我们在上面举的例子, 我们可以做一个简单的类比, Joint point 就相当于 爪哇的小县城里的百姓,pointcut 就相当于 老王所做的指控, 即凶手是个男性, 身高约七尺五寸, 而 Advice 则是施加在符合老王所描述的嫌疑人的动作: 抓过来审问.
为什么可以这样类比呢?
-
Join point : 爪哇的小县城里的百姓: 因为根据定义, Joint point 是所有可能被织入 Advice 的候选的点, 在 Spring AOP中, 则可以认为所有方法执行点都是 Joint point. 而在我们上面的例子中, 命案发生在小县城中, 按理说在此县城中的所有人都有可能是嫌疑人.
-
Pointcut :男性, 身高约七尺五寸: 我们知道, 所有的方法(joint point) 都可以织入 Advice, 但是我们并不希望在所有方法上都织入 Advice, 而 Pointcut 的作用就是提供一组规则来匹配joinpoint, 给满足规则的 joinpoint 添加 Advice. 同理, 对于县令来说, 他再昏庸, 也知道不能把县城中的所有百姓都抓起来审问, 而是根据凶手是个男性, 身高约七尺五寸, 把符合条件的人抓起来. 在这里 凶手是个男性, 身高约七尺五寸 就是一个修饰谓语, 它限定了凶手的范围, 满足此修饰规则的百姓都是嫌疑人, 都需要抓起来审问.
-
Advice :抓过来审问, Advice 是一个动作, 即一段 Java 代码, 这段 Java 代码是作用于 point cut 所限定的那些 Joint point 上的. 同理, 对比到我们的例子中, 抓过来审问 这个动作就是对作用于那些满足 男性, 身高约七尺五寸 的爪哇的小县城里的百姓.
-
Aspect:Aspect 是 point cut 与 Advice 的组合, 因此在这里我们就可以类比: “根据老王的线索, 凡是发现有身高七尺五寸的男性, 都要抓过来审问” 这一整个动作可以被认为是一个 Aspect.
AspectJ指示器
可参考官方文档:
切入点表达式解释:
AspectJ注解
注解 | 通知 |
---|---|
@After | 通知方法会在目标方法返回或抛出异常后调用 |
@AfterReturning | 通知方法会在目标方法返回后调用 |
@AfterThrowing | 通知方法会在目标方法抛出异常后调用 |
@Around | 通知方法会将目标方法包裹起来 |
@Before | 通知方法会再目标方法调用之前执行 |
Aop配置元素
spring的Aop配置元素能够以非侵入性的方式声明切面
Aop配置元素 | 用途 |
---|---|
<aop:advisor> | 定义Aop通知器 |
<aop:after> | 定义Aop后置通知(不管被通知的方法是否成功执行) |
<aop:after-returning> | 定义Aop返回通知 |
<aop:after-throwing> | 定义Aop异常通知 |
<aop:around> | 定义Aop环绕通知 |
<aop:aspect> | 定义一个切面 |
<aop:aspectj-autopoxy> | 启用@Aspect注解驱动的切面 |
<aop:before> | 定义Aop前置通知 |
<aop:config> | 顶层的Aop配置元素,大多数的aop:*元素必须包含在aop:config元素类 |
<aop:pointcut> | 定义一个切点 |
Java注解方式实现Aop
表演接口:
public interface Performance {
String perform();
}
音乐表演:
/**音乐表演*/
@Component
public class MusicPerformance implements Performance {
public String perform() {
System.out.println(">>>>>演员正在表演进行音乐演唱<<<<<");
//int i = 1/0;
return "MusicPerformance";
}
}
切面定义:
package com.ooyhao.spring.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* 描述:
* 类【PerformanceAspect】
*
* @author 阳浩
* @create 2019-08-29 17:55
*/
/*使用注解版*/
@Aspect
@Component
public class PerformanceAspect {
@Pointcut("execution(* *.perform(..))")
public void pointCut(){}
@Before("pointCut()")
public void offPhone(){
System.out.println("将手机关机或调为静音");
}
@After("pointCut()")
public void clean(){
System.out.println("清理座位旁边的垃圾");
}
@Around(value = "pointCut()")
public Object writeInfo(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("记录表演人员信息和歌曲名称");
Object result = joinPoint.proceed();
System.out.println("记录表演时间!");
return result;
}
@AfterThrowing(value = "pointCut()", throwing = "exception")
public void refund(JoinPoint joinPoint, Exception exception){
System.out.println(exception.getMessage());
System.out.println("观看不满意,要求退款");
}
@AfterReturning(value = "pointCut()" , returning = "result")
public void applause(JoinPoint joinPoint,Object result){
System.out.println("result:"+result);
System.out.println("起身并鼓掌");
}
}
测试类以测试结果:
@Test
public void testJavaConfigAop(){
AnnotationConfigApplicationContext context
= new AnnotationConfigApplicationContext(AopConfig.class);
Performance bean = context.getBean(Performance.class);
System.out.println(bean);
bean.perform();
}
/*
记录表演人员信息和歌曲名称
将手机关机或调为静音
>>>>>演员正在表演进行音乐演唱<<<<<
记录表演时间!
清理座位旁边的垃圾
result:MusicPerformance
起身并鼓掌
*/
由上述的测试结果可以看出,通知的执行流程是:
当出现异常时:
目标方法执行时出现异常:
@Component
public class MusicPerformance implements Performance {
public void perform() {
System.out.println(">>>>>演员正在表演进行音乐演唱<<<<<");
int i = 1/0;
}
}
异常时执行结果:
记录表演人员信息和歌曲名称
将手机关机或调为静音
>>>>>演员正在表演进行音乐演唱<<<<<
清理座位旁边的垃圾
观看不满意,要求退款
java.lang.ArithmeticException: / by zero
由上述两个流程图可以看出:
正常情况时:
环绕通知目标方法前-->前置通知-->目标方法-->环绕通知目标方法后-->后置通知-->返回通知。
异常情况时:
环绕通知目标方法前-->前置通知-->目标方法-->后置通知-->异常通知。
总结:
正常情况下,不会执行异常通知(AfterTrowing),异常情况下,不会执行环绕通知目标方法后的代码(Around after),也不会执行返回通知(AfterReturning)。
Xml配置方式实现Aop
切面:使用Xml方式,切面就是一个普通的Java类
package com.ooyhao.spring.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* 描述:
* 类【PerformanceAspect】
*
* @author 阳浩
* @create 2019-08-29 17:55
*/
/*使用XML版*/
public class PerformanceAspect {
//before
public void offPhone(JoinPoint joinPoint){
System.out.println("将手机关机或调为静音");
}
//after
public void clean(JoinPoint joinPoint){
System.out.println("清理座位旁边的垃圾");
}
//around
public Object writeInfo(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("记录表演人员信息和歌曲名称");
Object result = joinPoint.proceed();
System.out.println("记录表演时间!");
return result;
}
//afterTrowing
public void refund(JoinPoint joinPoint, Exception exception){
System.out.println(exception.getMessage());
System.out.println("观看不满意,要求退款");
}
//afterReturning
public void applause(JoinPoint joinPoint, Object result) {
System.out.println("AfterReturning :result "+result);
System.out.println("起身并鼓掌");
}
}
Xml配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--目标对象-->
<bean class="com.ooyhao.spring.bean.MusicPerformance"/>
<!--开启aop的自动代理-->
<aop:aspectj-autoproxy/>
<!--将切面定义为一个Bean-->
<bean id="performanceAspect" class="com.ooyhao.spring.aop.PerformanceAspect"/>
<!--通知定义-->
<aop:config>
<aop:aspect ref="performanceAspect">
<aop:pointcut id="pointCut" expression="execution(* *.perform(..))"/>
<aop:before method="offPhone" pointcut-ref="pointCut"/>
<aop:after method="clean" pointcut-ref="pointCut"/>
<aop:around method="writeInfo" pointcut-ref="pointCut"/>
<aop:after-returning method="applause" pointcut-ref="pointCut" returning="result" />
<aop:after-throwing method="refund" pointcut-ref="pointCut" throwing="exception"/>
</aop:aspect>
</aop:config>
</beans>
测试及结果:
@Test
public void testXmlAop(){
ClassPathXmlApplicationContext context
= new ClassPathXmlApplicationContext("springAop.xml");
Performance performance = context.getBean(Performance.class);
performance.perform();
}
/**
将手机关机或调为静音
记录表演人员信息和歌曲名称
>>>>>演员正在表演进行音乐演唱<<<<<
起身并鼓掌
记录表演时间!
清理座位旁边的垃圾
*/
注意:可以看出,使用Java配置的方式和Xml配置的方式,通知执行顺序有差异。
JoinPoint 对象
JoinPoint
JoinPoint对象封装了SpringAop中切面方法的信息,在切面方法中添加JoinPoint参数,就可以获取到封装了该方法的JoinPoint对象。
常用API:
方法名 | 功能 |
---|---|
Signature getSignature() | 获取封装了署名信息的对象,在该对象中可以获取目标方法的方法名,所属类的Class等信息。 |
Object[] getArgs() | 获取传入目标方法的参数对象 |
Object[] getTarget() | 获取被代理的对象 |
Object[] getThis() | 获取代理对象 |
ProceedingJoinPoint
ProceedingJoinPoint 对象是JoinPoint的子接口,该对象只用在@Around的切面方法中,添加了两个方法:
Object proceed() trows Trowable //执行目标方法
Object proceed(Object[] var1) throws Throwable //传入的新的参数去执行目标方法
案例说明
User类:
public class User {
private String name;
private Integer age;
}
UserService类:
@Component
public class UserService {
public void Login(User user,String authCode){
System.out.println("user: "+user+" authCode: "+authCode);
}
}
切面类:
@Component
@Aspect
public class UserAspect {
@Pointcut("execution(* *Login(..))")
public void pointCut(){}
/**
* 目标方法:
* public class UserService {
*
* public void Login(User user,String authCode){
* System.out.println("user: "+user+" authCode: "+authCode);
* }
* }
* */
@Around("pointCut()")
public Object checkPermission(ProceedingJoinPoint joinPoint) throws Throwable {
//=================joinPoint.getArgs()==============================
//目标方法的入参 [User{name='张三', age=23}, 123456]
Object[] args = joinPoint.getArgs();
System.out.println(Arrays.toString(args));
// ================joinPoint.getSignature()=========================
Signature signature = joinPoint.getSignature();
//方法名 Login
String name = signature.getName();
System.out.println(name);
//目标方法所在类的Class对象 class com.ooyhao.spring.service.UserService
Class aClass = signature.getDeclaringType();
System.out.println(aClass);
//目标方法所在类的类的权限类名 com.ooyhao.spring.service.UserService
String typeName = signature.getDeclaringTypeName();
System.out.println(typeName);
//目标方法的修饰符
int modifiers = signature.getModifiers();
System.out.println(modifiers);
//=====================joinPoint.getTarget()===================
//被代理的目标对象 com.ooyhao.spring.service.UserService@1ba9117e
Object target = joinPoint.getTarget();
System.out.println(target);
//=====================joinPoint.getThis()===================
//代理对象
Object aThis = joinPoint.getThis();
System.out.println(aThis);
//可以将原有调用时传入的参数进行修改
// 调用无参的方法,即表示使用调用者传入的参数。
Object obj = joinPoint.proceed(new Object[]{new User("李四",24),"123abc"});
return obj;
}
}
配置类:
@ComponentScan(basePackages = "com.ooyhao.spring")
@EnableAspectJAutoProxy
public class UserAopConfig {}
对现有类增加方法
至此,SpringAop的JavaConfig配置类和Xml配置文件形式都已经学完,但是Aop中 @Before、@After、@Around、@AfterReturning、@AfterTrowing这几种通知都是只对目标类的目标方法进行增强,但是无法向目标方法注入新的方法。这么强大的Spring,肯定有相应的解决办法啦!那就是使用@DeclareParents 注解实现。
Java配置类方式
学生接口:
public interface Student {
void readBook();
}
学生实现类:
@Component
public class CollegeStudent implements Student {
public void readBook() {
System.out.println("我在阅读大学必修书籍!");
}
}
教师接口:
/*教师接口*/
public interface Teacher {
void speak();
}
教师实现类:
public class EnglishTeacher implements Teacher {
public void speak() {
System.out.println("我会说英语!");
}
}
切面:
@Aspect
@Component
public class StudentAspect {
@DeclareParents(value = "com.ooyhao.spring.bean.Student+",defaultImpl = EnglishTeacher.class)
private Teacher teacher;
}
配置类:
@ComponentScan(basePackages = "com.ooyhao.spring")
@EnableAspectJAutoProxy
public class AopConfig {}
单元测试:
@Test
public void testAop(){
AnnotationConfigApplicationContext context
= new AnnotationConfigApplicationContext(AopConfig.class);
Student bean = context.getBean(Student.class);
bean.readBook();
Teacher t = (Teacher)bean;
t.speak();
}
结果:
解释:首先教师和学生都是一个普通的java类,切面类中依旧使用@Aspect注解来定义其为一个切面类,使用@Component标注为一个Spring组件。而在配置类中使用@ComponentScan注解用来对组件进行扫描。使用@EnableAspectJAutoProxy 开启AspectJ自动代理。
需要研究的是切面中的内容:
@DeclareParents(value = "com.ooyhao.spring.bean.Student+",
defaultImpl = EnglishTeacher.class)
private Teacher teacher;
属性teacher表示将哪种类型声明为增加类。而使用@DeclareParents注解来声明需要增加和实际定义了增加方法的实际类。其中value表示向所有Student类及其子类增加方法,增加的方法的实际来源是在defaultImpl中定义的,即:增加的方法在EnglishTeacher中定义。并且在实际类型转化的时候,不能将测试代码中的bean强转为EnglishTeacher,只能强转为Teacher类型。
解释:@DeclareParents
注解由三部分组成:
- value 属性指定了哪种类型的bean要引入该接口。(标记符后面的加号,表示的是所有的子类,而不是其自身。)
- defaultImpl 属性指定了为引入功能提供实现的类。
- @DeclareParents 注解所标注的静态属性指明了要引入的接口。
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"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:mvc="http://www.springframework.org/schema/mvc"
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
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!--开启包的扫描-->
<context:component-scan base-package="com.ooyhao.spring"/>
<!--声明为一个Bean,即定义了增加方法的一个类-->
<bean id="englishTeacher"
class="com.ooyhao.spring.bean.EnglishTeacher"/>
<!--切面-->
<aop:config>
<aop:aspect>
<aop:declare-parents
types-matching="com.ooyhao.spring.bean.Student+"
implement-interface="com.ooyhao.spring.bean.Teacher"
delegate-ref="englishTeacher"/>
</aop:aspect>
</aop:config>
<!--开启AspectJ的自动代理-->
<aop:aspectj-autoproxy/>
</beans>
单元测试:在获取Bean的时候,下列代码中只能获取Student类型,不能获取Student实现类CollegeStudent类型的Bean。
@Test
public void testXmlAop(){
ClassPathXmlApplicationContext context
= new ClassPathXmlApplicationContext("AopConfig.xml");
Student bean = context.getBean(Student.class);
bean.readBook();
Teacher teacher = (Teacher)bean;
teacher.speak();
}
本节主要是学习SpringAop的基于Java配置和Xml配置两种方式的使用方法,以及SpringAop中五种通知做不到的,就是在目标类中添加方法,SpringAop中的五种通知只能增强方法,而不能添加方法到目标类中,SpringAop提供了另外一种解决方案:@DeclareParents
.
源码地址:
https://gitee.com/ooyhao/JavaRepo_Public/tree/master/Spring-in-Action/spring-in-action-04
最后
如果觉得不错的话,那就关注一下小编哦!一起交流,一起学习