《Spring in action 4》(四)面向切面的Spring

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6


面向切面的Spring

文章目录

  • ​​面向切面的Spring​​
  • ​​Aop 的概念​​
  • ​​Aop中的术语​​
  • ​​通知(Advice)​​
  • ​​切面(Aspect)​​
  • ​​连接点(Joint Point)​​
  • ​​切点(PointCut)​​
  • ​​目标对象(Target)​​
  • ​​织入(Weaving)​​
  • ​​案例解释术语​​
  • ​​AspectJ指示器​​
  • ​​AspectJ注解​​
  • ​​Aop配置元素​​
  • ​​Java注解方式实现Aop​​
  • ​​Xml配置方式实现Aop​​
  • ​​JoinPoint 对象​​
  • ​​JoinPoint​​
  • ​​ProceedingJoinPoint​​
  • ​​案例说明​​
  • ​​对现有类增加方法​​
  • ​​Java配置类方式​​
  • ​​Xml配置文件方式​​
  • ​​最后​​

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的过程。

案例解释术语

​ 看到上面的术语其实非常的头痛,不知所云,那么下面用一个比较容易理解的例子来说明上述概念:

下面我以一个简单的例子来比喻一下 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指示器

可参考官方文档:

​https://docs.spring.io/spring/docs/5.1.9.RELEASE/spring-framework-reference/core.html#aop-aspectj-support​

切入点表达式解释:

《Spring in action 4》(四)面向切面的Spring_Spring

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
起身并鼓掌
*/

由上述的测试结果可以看出,通知的执行流程是:

《Spring in action 4》(四)面向切面的Spring_spring_02

当出现异常时:

目标方法执行时出现异常:

@Component
public class MusicPerformance implements Performance {

public void perform() {
System.out.println(">>>>>演员正在表演进行音乐演唱<<<<<");
int i = 1/0;
}
}

异常时执行结果:

记录表演人员信息和歌曲名称
将手机关机或调为静音
>>>>>演员正在表演进行音乐演唱<<<<<
清理座位旁边的垃圾
观看不满意,要求退款

java.lang.ArithmeticException: / by zero

《Spring in action 4》(四)面向切面的Spring_System_03

由上述两个流程图可以看出:

正常情况时:

环绕通知目标方法前–>前置通知–>目标方法–>环绕通知目标方法后–>后置通知–>返回通知。
异常情况时:

环绕通知目标方法前–>前置通知–>目标方法–>后置通知–>异常通知。

总结:

正常情况下,不会执行异常通知(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();
}

结果:

《Spring in action 4》(四)面向切面的Spring_Java_04

解释:首先教师和学生都是一个普通的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​

最后

如果觉得不错的话,那就关注一下小编哦!一起交流,一起学习


阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: Spring