Spring切面学习

10 篇文章 0 订阅

1. 介绍

Spring提供了一种面向切面的编程,内部的实现是一种基于Cglib的代理模式,使用切面,可以在不更改源码的情况下,简化大量的无用重复代码,使代码更关注实现业务逻辑,而不需要考虑是否需要配置日志、安全验证等。


2. 环境配置

本文采用Maven进行搭建环境,如果之前没使用过Maven,请下载相应的jar包放入项目的lib库即可。
以下是所用到的pom文件:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.0.8.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.0.10.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.1.3.RELEASE</version>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>

3. 切面介绍

3.1 通知(Advice)

通知是定义了切面是什么以及何时使用,除了描述切面要完成的工作,通知还解决了需要什么时候执行这个工作的问题
Spring切面有5种类型的通知:

  • 前置通知Before:在方法调用之前触发
  • 后置通知After:在方法调用之后触发
  • 返回通知AfterReturning:在方法正常返回之后触发
  • 异常通知AfterThrowing:在方法抛出异常之后触发
  • 环绕通知Around:在方法调用前和调用后触发,由于和上述四种不同,Spring对此专门做了一层包装,因此也可以理解为,Around可以替代上述四种。

3.2 连接点(JoinPoint)

连接点是应用在执行过程中能够插入切面的一个点。这个点可以是在调用方法时、抛出异常时、甚至修改一个字段时。

3.3 切点(PointCut)

切点定义了何处执行,切点会匹配通知所需要织入的一个或多个连接点。

3.4 切面(Aspect)

切面由通知+切点构成,可定义为:它是什么,需要在什么时候执行,以及何处完成这个功能。

3.5 织入(Weaving)

织入是把切面应用到目标对象并创建新的代理对象的过程。


4. 切面语法介绍

4.1 语法介绍

语法:execution([修饰符]  返回值  包.类.方法名(参数) throws异常)
  • 修饰符可选择,一般为public,也可以设置为*,表示任意
  • 返回值:不能省略
    • void:不返回内容
    • int/double/Object:返回对应的内容
    • *:不指定需要返回的内容类型,匹配任意返回
  • 包:
  • 类:
    • UserEntity:直接给定固定类
    • *Entity:所有以Entity的类
    • User*:所有以User开头的类
    • *:任意类
  • 方法名:
    • addUser:直接给定方法名
    • add*:以add开头的方法
    • *User:以User结尾的方法
    • *:任意方法
  • 参数:
    • ():无参
    • (int):需要传递一个int类型的参数
    • (int,int):需要传递两个int类型的参数
    • (…):任意参数
  • throws:可省略,一般不写

4.2 演示小例子

execution(public * com.test.aspect.Divided.*(..))
说明:方法修饰符必须为public,返回值任意,扫描com.test.aspect包下的Divided类的任意方法,参数不限制


5. 基础用法

新建一个测试类:

public class Divided {

	public int divide(int a, int b) {
		return a / b;
	}
}

新建切面:本例子用于计算测试类执行方法消耗的时间

@Aspect
public class DividedAspect {

	private long startTime;

	@Pointcut(value = "execution(public * aspect.Divided.*(..))")
	public void pointCut() {}

	@Before("pointCut()")
	public void logStart() {
		System.out.println("日志开始");
		startTime = System.currentTimeMillis();
	}

	@After("pointCut()")
	public void logEnd() {
		System.out.println("日志结束");
		System.out.println("运行时间:" + (System.currentTimeMillis() - startTime) + "ms!");
	}

	@AfterReturning(value = "pointCut()")
	public void logReturn() {
		System.out.println("日志正常返回,返回结果!");
	}

	@AfterThrowing(value = "pointCut()")
	public void logException() {
		System.out.println("异常信息:");
	}

}

新建JavaConfig文件

@Configuration
@EnableAspectJAutoProxy//需要使用该注解,启动Spring对AspectJ的支持
public class BeanAspectAnnotation {

	@Bean
	public Divided createDivided() {
		return new Divided();
	}

	@Bean//将切面类加入Spring容器进行管理
	public DividedAspect createAspect() {
		return new DividedAspect();
	}
}

新建测试类

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = BeanAspectAnnotation.class)
public class BeanAspectAnnotationTest extends TestCase {
	@Autowired
	private Divided divided;

	public void testCreateDivided() {
		divided.divide(10, 1);
	}
}

测试结果


日志开始
日志结束
运行时间:10ms!
日志正常返回,返回结果!

6. 高级用法

6.1 获得运行环境时的参数

Spring提供了获得运行时参数的对象JoinPoint,我们可以将我们之前的例子进行改编一下:

@Aspect
public class DividedAspect {

	private long startTime;

	@Pointcut(value = "execution(public * aspect.Divided.*(..))")
	public void pointCut() {}

	@Before("pointCut()")
	public void logStart(JoinPoint joinPoint) {
		System.out.println("日志开始");
		startTime = System.currentTimeMillis();
		Object[] args = joinPoint.getArgs();
		System.out.println("参数列表:" + Arrays.asList(args));
		String methodName = joinPoint.getSignature().getName();
		System.out.println("方法名称:" + methodName);
		String kind = joinPoint.getKind();
		System.out.println("类型:" + kind);
		String longString = joinPoint.toLongString();
		System.out.println("连接点表达式:" + longString);
	}

	@After("pointCut()")
	public void logEnd() {
		System.out.println("日志结束");
		System.out.println("运行时间:" + (System.currentTimeMillis() - startTime) + "ms!");
	}

	@AfterReturning(value = "pointCut()", returning = "result")
	public void logReturn(Object result) {
		System.out.println("日志正常返回,返回结果:" + result);
	}

	@AfterThrowing(value = "pointCut()", throwing = "exception")
	public void logException(Exception exception) {
		System.out.println("异常信息:" + exception);
	}

}

我们在被@Before@AfterReturning@AfterThrowing三个注解修饰的方法中,添加了参数,其中@Before中可添加的参数与@After是一致的,因此在此使用@Before进行讲解。

  • @Before

    • joinPoint.getArgs():获得参数列表
    • joinPoint.getSignature().getName():获得方法名称
    • joinPoint.getKind():获得方法类型
    • joinPoint.toLongString():获得连接点表达式
  • @AfterReturning

    • returning = "result":必须指定需要返回的参数名称,否则无法得到对应的参数
  • @AfterThrowing

    • throwing = "exception":必须指定需要抛出的异常信息
日志开始
参数列表:[10, 1]
方法名称:divide
类型:method-execution
连接点表达式:execution(public int aspect.Divided.divide(int,int))
日志结束
运行时间:10ms!
日志正常返回,返回结果:10

6.2 使用环绕通知代替上述四大方法

修改切面类

@Aspect
public class DividedAspect {

	@Pointcut(value = "execution(public * aspect.Divided.*(..))")
	@Around(value = "pointCut()")
	public Object logAround(ProceedingJoinPoint proceedingJoinPoint) {
		System.out.println("环绕通知开始!");
		try {
			return proceedingJoinPoint.proceed();
		} catch (Throwable throwable) {
			throwable.printStackTrace();
		}
		System.out.println("环绕通知结束!");
		return null;
	}
}

此处实现的原理与上面是一致的,但是此处使用了ProceedingJoinPoint,这是Spring进行包装的一层代理,并且需要通过调用proceedingJoinPoint.proceed(),最终结果才能正确被得到。

6.3 不修改原有代码前提下,扩展方法

假设:我们现在需要对Divided类进行扩展,但是又不能修改原有代码,那么我们可以通过扩展接口来实现功能的扩展。

新建MathFunction接口:

public interface MathFunction {

	int multiply(int a, int b);
}

新建接口实现类:

public class MultiImpl implements MathFunction {

	@Override
	public int multiply(int a, int b) {
		return a * b;
	}
}

扩展Aspect:

@Aspect
public class DividedAspect {

	// 省略其他代码

	@DeclareParents(value = "aspect.Divided+",defaultImpl = MultiImpl.class)
	public MathFunction mathFunction;
}

解释:
value:表示将要修饰的类,后面的+表示修饰不仅仅是类本身,还包括子类
defaultImpl:表示具体的实现类

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = BeanAspectAnnotation.class)
public class BeanAspectAnnotationTest extends TestCase {
	@Autowired
	private Divided divided;

	public void testCreateDivided() {
		divided.divide(10, 1);
		MathFunction mathFunction = (MathFunction) divided;
		int result = mathFunction.multiply(10, 2);
		System.out.println(result);
	}
}

测试结果:

日志开始
参数列表:[10, 1]
方法名称:divide
类型:method-execution
连接点表达式:execution(public int aspect.Divided.divide(int,int))
日志结束
运行时间:10ms!
日志正常返回,返回结果:10
20

注意:被新方法修饰的方法,如果没有添加切面,那么一样无法使用切面。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值