[火眼速查] Spring 速查指南(四)

简介

Spring 是一款开源的 J2EE 框架,它有许多项目,为 Java 应用开发提供了一整套的工具,其中最核心的就是 Spring Framework 和 Spring Boot 项目。

文本是一个系列文章的第一篇,下面就这两个项目的核心内容做一些速查整理,同时辅以生产源码,便于理解。

相关文章

面向切面编程(AOP)

面向切面编程(Aspect-oriented Programming,AOP)与面向对象编程(Object-oriented Programming,OOP)的思考模式不一样,面向对象编程的关注点是类,而面向切面编程的关注点是切面。AOP 可以将一些功能(如事务管理、日志记录等)模块化,与业务功能解耦,实现代码复用,提高代码的可扩展性和可维护性。

AspectJ 是一个很成熟的 AOP 实现,而 Spring AOP 指的是 Spring 框架提供的 AOP 能力,Spring 的 IoC 容器并不强依赖于 AOP,但使用 AOP 能够让 Spring IoC 容器提供强大的中间件解决方案。

AOP 的实现分为静态和动态两种,静态 AOP 在编译期对源代码进行修改,生成的字节码已经被代理,如比较成熟的 AspectJ 框架。而动态 AOP 则在运行时生成动态代理对象,Spring AOP 就采用了这种方式。

Spring AOP 主要提供了:

  • 声明式服务,其中最重要的就是事务管理
  • 让用户自定义切面,与面向对象编程互补

AOP 核心概念

AOP 中有几个核心概念:

  • 切面(Aspect):横跨多个类的模块化的关注点,简单理解就是对多个类的相同的功能抽取成模块化,包含了切入点和通知。Spring AOP 实现的方式有使用普通类(基于模式的方法,schema-based)和使用注解方式(@AspectJ 风格)。
  • 连接点(Join point):程序执行中特定的点,例如方法调用或抛出异常。在 Spring AOP 中,连接点只是方法调用。
  • 通知(Advice):通知就是切面在连接点上的操作。
  • 切入点(Pointcut):切入点代表了一组连接点,通过切入点表达式来匹配。
  • 简介(Introduction):给被增强的对象引入一个接口和实现类,给对象注入新的成员变量或成员方法,也就是 AspectJ 的 inter-type declaration。
  • 目标对象(Target object):目标对象就是被切面增强的对象,由于 Spring AOP 使用运行时代理实现,这个对象总是被代理对象。
  • AOP 代理(AOP proxy):AOP 代理是由 AOP 框架创建的实现切面增强的对象,在 Spring AOP 中使用 JDK 动态代理或 CGLIB 代理。
  • 织入(Weaving):织入是将切面和目标对象连接起来创建代理对象的过程,一般可以在编译时和运行时。Spring AOP 框架采用了运行时织入。

概括来说就是,切面通过切入点找到目标对象上的连接点,并把通知(增强代码)织入生成新的 AOP 代理对象。我们调用目标对象的方法,其实是调用 AOP 代理对象的方法,这样就可以在不改变原目标对象的前提下,增强它的功能。

如果觉得上面的描述不好理解,在 Spring AOP 中,我们可以做如下简化理解。

  • 切面:一个添加了特定注解的 Bean,包含了切入点和通知。
  • 连接点:就是我们需要增强的方法。
  • 通知:我们需要对目标方法增强的功能。
  • 切入点:找到连接点方法的表达式。

而实现的方式就是生成一个代理对象,替我们去操作目标对象。

例如,普通调用一个对象的方法像这样。
普通调用

当有代理时,调用就变成了这样。
通过代理调用

通知就是对连接点(目标对象方法)的增强,Spring AOP 提供了以下几种通知类型:

  • 前置通知(Before advice):在连接点前执行,但不能终止后续代码的执行(除非抛出异常)。
  • 返回通知(After returning advice):在连接点正常完成执行并返回后执行(未抛出异常的情况下)。
  • 异常通知(After throwing advice):方法执行抛出异常时执行。
  • 后置通知(After advice):在连接点之后执行,不管是正常完成还是抛出异常。
  • 环绕通知(Around advice):在连接点的调用前后执行,这是最强大的通知,它可以选择继续执行连接点或者终止(直接返回结果或抛出异常)。

虽然环绕通知可以替代其他通知,但是 Spring 官方还是建议使用最小化通知,以避免产生不必要的错误。

Spring AOP 由纯 Java 实现,只支持方法调用连接点。字段拦截并未实现,如果需要字段拦截能力,可以使用其他 AOP 框架如 AspectJ。

AOP 的关键就是由切入点来匹配连接点,对于 Spring AOP 来说,就是找到特定的方法调用,来增强这个方法。

@AspectJ 注解方式

Spring AOP 支持 @AspectJ 的注解方式,可以使用 @AspectJ 提供的注解来使用 AOP,但仍是由 Spring AOP 来实现的。要启用的话先使用注解 @EnableAspectJAutoProxy 开启。

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}

声明切面

开启后就可以使用 @Aspect 注解标记类来声明切面,在 Spring 中,切面类可以是一个 Bean。

声明切入点

在 Spring AOP 中,连接点只能是方法调用,因此切入点就是找到需要增强的方法。在切面类中使用 @Pointcut 注解来声明切入点。

@Pointcut("execution(* transfer(..))") // 切入点表达式
private void anyOldTransfer() {} // 切入点签名

我们主要就是通过切入点表达式来匹配要增强的方法。Spring AOP 支持如下的 AspectJ 切入点代码(AspectJ pointcut designators,PCD):

  • execution:用于匹配方法执行的连接点,这是 Spring AOP 最主要的 PCD。
  • within:限制匹配的连接点在特定的类型中。
  • this:限制匹配的连接点的代理对象是某个类型的实例。
  • target:限制匹配的连接点的目标对象是某个类型的实例。
  • args:限制匹配的连接点的参数是某个类型的实例。
  • @target:限制匹配的连接点的目标对象的类具有某个类型的注解。
  • @args:限制匹配的连接点的运行时实际参数具有某个类型的注解。
  • @within:限制匹配的连接点具有指定的注解。
  • @annotation:限制匹配的连接点上的注解
  • bean:Spring AOP 额外提供的可以用于限制连接点的 Bean 名称,支持通配符(*)。

其他一些 AspectJ 的 PCD 则不被 Spring AOP 所支持。

语法

最常用的 execution 语法如下所示:

execution(修饰符? 返回类型 类型声明? 方法名(参数) 异常?)

其中,返回类型、方法名、参数是必填的,其他可选,返回类型、类型声明和方法名中可以使用通配符(*),参数 () 代表没有参数,而 (..) 则代表任意参数。类型名都要写全限定名称。

// 任意 public 方法
execution(public * *(..))

// 以 set 开头的任意方法
execution(* set*(..))

// AccountService 接口里定义的任意方法
execution(* com.xyz.service.AccountService.*(..))

// com.xyz.service 包下的任意方法
execution(* com.xyz.service.*.*(..))

// com.xyz.service 包及其子包下的任意方法
execution(* com.xyz.service..*.*(..))

within 则更简单,可以限制类型。

// com.xyz.service.* 包下的任意方法
within(com.xyz.service.*)

// com.xyz.service 包及其子包下的任意方法
within(com.xyz.service..*)

this 则匹配代理对象。

// 代理对象实现 AccountService 接口的任意方法
this(com.xyz.service.AccountService)

target 则匹配目标对象。

// 目标对象实现 AccountService 接口的任意方法
target(com.xyz.service.AccountService)

args 匹配方法的参数类型。

// 匹配单个参数且运行时传递过来是 Serializable 类型的方法
args(java.io.Serializable)

和 execution 语法 execution(* *(java.io.Serializable)) 不同的是,args 匹配的是运行时的类型,而 execution 匹配的是签名中的类型。

@target 匹配目标对象的注解。

// 目标对象具有 Transactional 注解的任意方法
@target(org.springframework.transaction.annotation.Transactional)

@args 匹配运行时的方法注解。

// 匹配单个参数且运行时传递过来具有 Classified 注解的方法
@args(com.xyz.security.Classified)

@within 匹配类上的注解。

// 匹配具有 Transactional 注解的类的任意方法
@within(org.springframework.transaction.annotation.Transactional)

@target / @within 和 target / within 两对很相似,主要的区别就是 @within / within 匹配的类型,如果有继承的子类(没有覆盖方法)也会匹配, 而 @target / target 匹配的是运行时的对象,不会考虑子类。

例如:

@A1
public class Person {

    public void say(String sentence) {
        System.out.println("Person says:" + sentence);
    }

    public void run() {
        System.out.println("Person runs." );
    }
}

public class Student extends Person {

    @Override
    public void run() {
        System.out.println("Student runs." );
    }

    public void learn() {
        System.out.println("Student learn." );
    }
}

有如下的切入点表达式:

// 匹配 Person 类的所有方法和 Student 类未覆盖的方法,不会匹配 Student 类覆盖的 run 和新加的 learn
@within(com.annotation.other.A1)

// 匹配 Person 类的所有方法,而 Student 类没有注解 A1,都不会被匹配
@target(com.annotation.other.A1)

@annotation 匹配连接点的注解,也就是方法上的注解。

// 具有 @Transactional 注解的任意方法
@annotation(org.springframework.transaction.annotation.Transactional)

Bean 可以匹配 Bean 名称。

// Bean 名为 tradeService 的任意方法
bean(tradeService)

// Bean 名称以 Service 结尾的任意方法
bean(*Service)

切入点表达式可以使用 &&||! 逻辑运算符组合。

详细可参考 AspectJ 编程指南

声明通知

通知可以和切入点表达式相结合,在切面类中添加通知。

前置通知

在切面类中使用 @Before 注解声明前置通知。

@Aspect
public class BeforeExample {

	@Before("execution(* com.xyz.dao.*.*(..))")
	public void doAccessCheck() {
		// ...
	}
}

可以在注解中直接写切入点表达式,或者先定义切入点再引用,这样可以复用切入点。

@Aspect
public class BeforeExample {

    @Pointcut("execution(* com.xyz.dao.*.*(..))")
	public void dataAccessOperation() {}

    // 使用全限定名,如果是当前类中的则可以省略包名
	@Before("dataAccessOperation()")
	public void doAccessCheck() {
		// ...
	}
}
返回通知

在切面类中使用 @AfterReturning 声明返回通知,返回通知只在方法正常返回时执行。

@Aspect
public class AfterReturningExample {

	@AfterReturning("execution(* com.xyz.dao.*.*(..))")
	public void doAccessCheck() {
		// ...
	}
}

如果需要使用方法的返回值,可以使用 returning 参数指定,在通知方法的参数中获取。参数的类型也会限制匹配的方法,下面的 Object 则不限制。

@Aspect
public class AfterReturningExample {

	@AfterReturning(
		pointcut="execution(* com.xyz.dao.*.*(..))",
		returning="retVal")
	public void doAccessCheck(Object retVal) {
		// ...
	}
}
异常通知

在切面类中使用 @AfterThrowing 注解声明异常通知,当方法抛出异常时执行。

@Aspect
public class AfterThrowingExample {

	@AfterThrowing("execution(* com.xyz.dao.*.*(..))")
	public void doRecoveryActions() {
		// ...
	}
}

如果要限制并获取异常对象,可以使用 throwing 参数。

@Aspect
public class AfterThrowingExample {

	@AfterThrowing(
		pointcut="execution(* com.xyz.dao.*.*(..))",
		throwing="ex")
	public void doRecoveryActions(DataAccessException ex) {
		// ...
	}
}

注:连接点方法抛出的异常才会匹配,通知方法抛出的不会。

后置通知

在切面类中使用 @After 注解声明后置通知,后置通知必须要处理正常返回和抛出异常的场景。

@Aspect
public class AfterFinallyExample {

	@After("execution(* com.xyz.dao.*.*(..))")
	public void doReleaseLock() {
		// ...
	}
}
环绕通知

在切面类中使用 @Around 注解声明环绕通知,环绕通知在方法的前后都可以插入代码,并可以决定是否要真正执行连接点方法。虽然环绕通知可以替代之前的通知,但官方建议是最小化使用,因为环绕通知有很多注意事项,一不小心就会遗漏。

@Aspect
public class AroundExample {

	@Around("execution(* com.xyz..service.*.*(..))")
	public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
		// 前置处理,然后调用连接点方法
		Object retVal = pjp.proceed();
		// 后置处理
		return retVal;
	}
}

环绕通知的处理方法应该声明返回值类型为 Object,它的返回值会返回给连接点方法的调用方,如果环绕通知方法的返回值声明为 void,则返回 null 给调用方。

环绕通知的处理方法的第一个参数必须是 ProceedingJoinPoint 类型。在通知方法体中,需要调用 ProceedingJoinPoint 对象的 proceed() 方法来调用原连接点方法。如果调用 proceed() 时不提供参数,则使用原调用者提供的参数。当然可以主动提供参数来替换,proceed() 也接受 Object[] 数组作为参数,作为连接点方法的参数。

通知参数
获取连接点信息

每个通知处理方法可以声明第一个参数为 org.aspectj.lang.JoinPoint 类型,来获取连接点的信息(环绕通知的第一个参数是 ProceedingJoinPoint,是 JoinPoint 的子类)。

JoinPoint 接口提供了一些有用的方法:

  • getArgs():可获取连接点方法的参数
  • getThis():可获取代理对象
  • getTarget():可获取目标对象
  • getSignature():可获取被增强方法的描述
  • toString():输出连接点方法的描述信息

这里可以查询详细的 API。

获取参数信息

如果使用了 args 切入点表达式,则可以在通知方法中获取对应的参数,这样就可以根据传递的参数来进行处理。

@Before("execution(* com.xyz.dao.*.*(..)) && args(account,..)")
public void validateAccount(Account account) {
	// ...
}

其他的切入点表达式也可以获得对应的对象作为参数,this 获取代理对象,target 获取目标对象,@within、@target、@annotation 和 @args 则获取对应的注解对象。

参数范型

如果参数中包含范型,如下面的接口:

public interface Sample<T> {
	void sampleGenericMethod(T param);
	void sampleGenericCollectionMethod(Collection<T> param);
}

则可以通过参数类型来限制指定的类型才能被匹配。

@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
	// Advice implementation
}

但如果是范型集合,则无法生效,只能使用 Collection<?> 不限制类型,在通知方法中自行校验。

指定参数名

通知注解提供了 argNames 参数来指定通知函数的参数名。

@Before(
	value = "com.xyz.Pointcuts.publicMethod() && target(bean) && @annotation(auditable)",
	argNames = "bean,auditable")
public void audit(JoinPoint jp, Object bean, Auditable auditable) {
	AuditCode code = auditable.value();
	// ...
}
通知执行顺序

当有多个通知匹配到同一个连接点方法时,Spring AOP 和 AspectJ 采用了相同的策略。在连接点方法前,优先级从高到低执行,在连接点方法后,优先级从低到高执行。而在同一个切面类中,优先级从高到低分别是:@Around, @Before, @After, @AfterReturning, @AfterThrowing。也就是说,对于同一个连接点方法,@After 总是在 @AfterReturning 和 @AfterThrowing 之后执行。对于相同的通知或者在不同的切面类中,执行顺序是不确定的,建议使用 @Order 注解指定优先级,切面类也可以实现 Ordered 接口,通过 getOrder() 方法来指定优先级。

简介

简介(AspectJ 中称为 inter-type declarations)使被增强对象实现一个指定的接口,并提供一个接口的实现来注入成员变量或成员方法。

使用 @DeclareParents 注解来声明简介,假设有一个接口 UsageTracked,和这个接口的实现类 DefaultUsageTracked,下面的声明表示给所有 service 下的实现类都添加了一个 UsageTracked 接口。

@Aspect
public class UsageTracking {

	@DeclareParents(value="com.xyz.service.*+", defaultImpl=DefaultUsageTracked.class)
	public static UsageTracked mixin;

	@Before("execution(* com.xyz..service.*.*(..)) && this(usageTracked)")
	public void recordUsage(UsageTracked usageTracked) {
		usageTracked.incrementUseCount();
	}

}

简介提供了一种将目标对象和接口的实现类组合生成新的代理对象的方法,可以将一个功能提取到接口中,并在不修改目标类的情况下“复用”代码。

基于模式(Schema-based)的方式

Spring 也提供了基于 XML 的声明方式,切入点表达式的语法和通知类型都和上面的一致。要使用这种方式,需要引入 spring-aop 的 XML 模式,参考这里

<aop:config>

	<aop:aspect id="concurrentOperationRetry" ref="concurrentOperationExecutor">

		<aop:pointcut id="idempotentOperation"
			expression="execution(* com.xyz.service.*.*(..))"/>

		<aop:around
			pointcut-ref="idempotentOperation"
			method="doConcurrentOperation"/>

	</aop:aspect>

</aop:config>

<bean id="concurrentOperationExecutor"
	class="com.xyz.service.impl.ConcurrentOperationExecutor">
		<property name="maxRetries" value="3"/>
		<property name="order" value="100"/>
</bean>

建议优先使用注解的方式,这里就不多展开了。

AOP 代理

Spring AOP 使用 JDK 动态代理或 CGLIB 代理在运行时创建代理对象。JDK 动态代理是 JDK 内置的能力,而 CGLIB 则是一个开源库。如果目标对象实现了至少一个接口,则 Spring AOP 优先使用 JDK 动态代理,否则就使用 CGLIB 代理。

JDK 动态代理只能代理实现了接口的类,而 CGLIB 可以代理任何类。因为 JDK 动态代理通过接口反射实现,而 CGLIB 代理是通过生成一个被代理类的子类来拦截被代理类的方法调用实现。因此 CGLIB 不能代理声明为 final 类型的类和方法。一般来说,JDK 动态代理更简单,且是 JDK 直接支持的方式,因此 Spring 优先使用 JDK 动态代理。

如果要强制使用 CGLIB 代理,则可以如下配置:

注解方式

在 EnableAspectJAutoProxy 注解的参数 proxyTargetClass 设置为 true。

XML 方式

<aop:config proxy-target-class="true">
	<!-- other beans defined here... -->
</aop:config>

(未完待续)

如果觉得有用,请多多支持,点赞收藏吧!

  • 9
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
火眼证据分析是一款专业的电子取证软件,主要用于数字取证和取证分析。它提供了强大的取证功能和多种分析工具,帮助用户高效地从各种数字设备和电子媒体上提取、分析和还原目标数据。 要下载火眼证据分析,可以在CSDN(中国软件开发网)上进行操作。CSDN是一个为软件开发者提供学习、交流和资源下载的平台,拥有广泛的软件和开发相关资源。以下是在CSDN上下载火眼证据分析的步骤: 1. 打开浏览器,进入CSDN官网(www.csdn.net)。 2. 在网站首页的搜索栏中输入"火眼证据分析",点击搜索按钮。 3. 在搜索结果页面中,可以看到与"火眼证据分析"相关的内容和资源。找到适合自己的下载资源,并点击进入对应页面。 4. 在资源页面,可以阅读该软件的详细介绍和用户评价,确保其真实可靠。 5. 根据页面提供的下载链接,点击下载按钮来下载软件安装包。通常,CSDN提供多个下载通道供选择,用户可以根据自己的需求选择合适的下载方式。 6. 下载完成后,找到下载的安装包文件,并运行安装程序。 7. 根据安装向导的指示,逐步完成火眼证据分析软件的安装。 8. 安装完成后,可以双击桌面图标或从开始菜单中找到火眼证据分析,并打开软件开始使用。 总之,通过在CSDN上搜索并下载火眼证据分析软件,用户可以方便地获取该软件并进行电子取证和分析工作。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值