在开发 Spring Boot 应用时,我们经常会用到诸如@Transactional
、@Cacheable
、@Retryable
、@Validated
、@Async
等注解。通过这些注释,我们为 bean 注入了补充逻辑,例如将数据库操作封装在事务中或实现缓存机制。
然而,并不是每个人都想知道它们在幕后是如何工作的以及使用它们可能会出现什么问题。在这篇文章中,让我们踏上旅程,探索最流行的注释是如何工作的以及自调用问题是什么。
Spring Bean 和代理
当某些注释(例如@Transactional
)应用于 bean 中的方法时。Spring 创建代理对象来拦截实际的方法调用并在此之前或之后执行其他逻辑。
Spring 通过这些代理利用AOP(面向方面编程)来解决与主逻辑不同的横切关注点,例如日志记录、安全性和事务。这使我们能够保持核心业务逻辑的干净,并专注于它,而不会被其他事情分散注意力。
当我们看一个例子时,理解事物通常会更容易。因此,让我们考虑以下服务:
@Service
public class CacheableService {
@Cacheable(cacheNames = "cache")
public String getFromCache() {
return "This value will be moved to cache and next time used from there";
}
}
为了CacheableService
开始使用缓存,我们getFromCache()
用@Cacheable
注释标记了该方法。Spring通过引入补充逻辑来承担增强bean的任务:
- 如果缓存中不存在该值,则必须检索该值并将其放入缓存中。
- 如果该值已存在于缓存中,则必须直接从缓存中检索该值。
这个过程涉及到代理的使用。在运行时,Spring不仅创建一个新的实例CacheableService
,而且还生成一个代理类来伴随它。
简化的序列如下所示:
编辑
因此,Spring 生成一个代理对象,它镜像现有方法,同时合并补充逻辑。
如果类实现了接口,则代理对象也实现了该接口,这种情况下使用动态代理。否则,代理使用CGLib扩展目标类。
最流行的注解
代理的类是在运行时生成的。在我们的示例中CacheableService
,生成的代理如下所示:
public class CacheableService$$SpringCGLIB$$0 extends CacheableService implements SpringProxy, Advised, Factory {
...
private MethodInterceptor CGLIB$CALLBACK_0;
...
@Override
public final String getFromCache() {
//execute using method interceptor
}
...
}
我们可以看到使用了 CGLib,因为它CacheableService
没有实现任何接口,并且生成的代理只是对其进行了扩展。如果CacheableService
实现了某个接口,则会使用动态代理,并且它将实现相同的接口。
Spring 在代理中使用MethodInterceptors,这些拦截器负责在实际方法调用周围添加补充逻辑
让我们看看方法拦截器内部发生了什么,这些方法拦截器与最常见的注释一起使用:
@Cacheable
让我们从@Cacheable
注释开始,如本文中的示例所示CacheableService
。在这种情况下,创建的代理使用CacheInterceptor:
if (cacheHit != null && !hasCachePut(contexts)) {
// If there are no put requests, just use the cache hit
cacheValue = cacheHit.get();
returnValue = wrapCacheValue(method, cacheValue);
} else {
// Invoke the method if we don't have a cache hit
returnValue = invokeOperation(invoker);
cacheValue = unwrapReturnValue(returnValue);
}
@Transactional
方法拦截器的操作如下:它检查缓存中是否有特定值,如果找到则直接返回该值。如果缓存中不存在该值,拦截器将通过invokeOperation
(目标方法调用)协调其计算。计算完成后,拦截器将值存储在缓存中,然后返回它。
TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
Object retVal;
try {
// Target method invocation
retVal = invocation.proceedWithInvocation();
} catch (Throwable ex) {
// target invocation exception -> transaction rollback
completeTransactionAfterThrowing(txInfo, ex); // leads to transactionManager.rollback(...);
throw ex;
} finally {
cleanupTransactionInfo(txInfo);
}
我们可以看到,其实这并没有那么困难。在方法调用之前启动一个新事务,如果没有错误发生则提交;否则,会发生事务回滚。
@Retryable
当涉及到@Retryable
注释时,生成的代理在幕后利用RetryOperationsInterceptor和RetryTemplate :
while (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) {
try {
...
T result = retryCallback.doWithRetry(context);
doOnSuccessInterceptors(retryCallback, context, result);
return result;
} catch (Throwable e) {
registerThrowable(retryPolicy, state, context, e);
...
}
...
}
实际的目标方法在循环中执行,直到成功或超出错误限制。
@Validated
@NotBlank
如果我们使用 @Validated 注解来注解某些组件,并且某些方法参数也使用验证注解(如、@Min
、 )进行标记@Email
,则MethodValidationInterceptor将被添加到生成的代理中。拦截器的工作原理如下:
try {
result = execVal.validateParameters(target, methodToValidate, invocation.getArguments(), groups);
} catch (IllegalArgumentException ex) {
...
result = execVal.validateParameters(target, methodToValidate, invocation.getArguments(), groups);
}
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
@Async
在此注释的情况下,代理使用AsyncExecutionInterceptor和AsyncTaskExecutor调用目标方法:
if (CompletableFuture.class.isAssignableFrom(returnType)) {
return executor.submitCompletable(task);
} else if (... some other cases ...){
} else if (Future.class.isAssignableFrom(returnType)) {
return executor.submit(task);
} else if (void.class == returnType) {
executor.submit(task);
return null;
} else {
throw new IllegalArgumentException("Invalid return type for async method (only Future and void supported): " + returnType);
}
自调用问题
我们已经了解到,Spring 创建代理对象来镜像现有的 bean 方法,并在目标方法调用之前或之后执行补充逻辑。
但是,如果我们从同一个 bean 中的另一个方法调用标有注释的方法,会发生什么情况呢?让我们通过一个例子来探讨这个案例:
@Service
public class CacheableService {
public String selfInvoke() {
return getFromCache();
}
@Cacheable(cacheNames = "cache")
public String getFromCache() {
return "This value will be moved to cache and next time used from there";
}
}
现在,CacheableService
包含一个名为 的附加方法selfInvoke()
,该方法未标记任何注释。该方法只是调用getFromCache()
,这是一个用@Cacheable
注释标记并在代理中增强以利用缓存的方法。
那么,问题就来了:该selfInvoke()
方法会从缓存中检索值吗?
许多开发人员期望这应该可行并且缓存将被利用。然而,事实上,它并没有达到预期的效果。为了理解这一点,让我们分析一个包含 update CacheableService
、其代理和方法调用的序列图selfInvoke()
。
编辑
所以,当我们调用selfInvoke()
方法时,我们只访问getFromCache()
目标bean中的方法。但是,为此方法启用缓存所做的所有增强都存在于getFromCache()
代理内的重写方法中,不幸的是,该方法仍未被访问。
同样的问题也适用于其他注释,例如@Transactional
、@Retryable
、@Validated
、@Async
等。
事实上,有多种选择可以解决这个问题。一种方法涉及重构代码以防止自调用标记有注释的方法并仅从其他 bean 调用它们。或者,可以考虑过渡到 AspectJ 来创建代理(编译时编织),而不是依赖 CGLib 和动态代理。此外,还可以采用一种称为自注入的技术,其中 Bean 被注入到其自身中,并在注入的 Bean 上调用目标方法。然而,值得注意的是,后一种解决方案本质上是一种解决方法。
结论
@Transactional
在本文中,我们解释了 Spring 如何使用代理向标有、@Cacheable
、@Retryable
、@Validated
、@Async
等注释的方法添加额外的逻辑。
我们还通过一些代码示例深入了解了这些注释在幕后如何工作。
但是,使用代理有时会导致自调用问题等问题。我们已经讨论了它是什么以及解决它的方法。
掌握这些概念很重要,我真的希望您觉得这篇文章很有趣。
谢谢阅读!