Spring Boot注解:背后的原理和自调用问题
  KMmKYbIfxJ2Q 2023年11月02日 46 0


在开发 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";
    }
  
}

Spring Boot注解:背后的原理和自调用问题_拦截器

为了CacheableService开始使用缓存,我们getFromCache()@Cacheable注释标记了该方法。Spring通过引入补充逻辑来承担增强bean的任务:

  • 如果缓存中不存在该值,则必须检索该值并将其放入缓存中。
  • 如果该值已存在于缓存中,则必须直接从缓存中检索该值。

这个过程涉及到代理的使用。在运行时,Spring不仅创建一个新的实例CacheableService,而且还生成一个代理类来伴随它。

简化的序列如下所示:

Spring Boot注解:背后的原理和自调用问题_缓存_02

Spring Boot注解:背后的原理和自调用问题_拦截器_03编辑

因此,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
    }
    ...
}

Spring Boot注解:背后的原理和自调用问题_缓存_04

我们可以看到使用了 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);
}

Spring Boot注解:背后的原理和自调用问题_方法调用_05

@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);
}

Spring Boot注解:背后的原理和自调用问题_缓存_06

我们可以看到,其实这并没有那么困难。在方法调用之前启动一个新事务,如果没有错误发生则提交;否则,会发生事务回滚。

@Retryable

当涉及到@Retryable注释时,生成的代理在幕后利用RetryOperationsInterceptorRetryTemplate :

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);
        ...
    }
    ...
}

Spring Boot注解:背后的原理和自调用问题_方法调用_07

实际的目标方法在循环中执行,直到成功或超出错误限制。

@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);
}

Spring Boot注解:背后的原理和自调用问题_缓存_08

@Async

在此注释的情况下,代理使用AsyncExecutionInterceptorAsyncTaskExecutor调用目标方法:

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 Boot注解:背后的原理和自调用问题_缓存_09

自调用问题

我们已经了解到,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";
    }
  
}

Spring Boot注解:背后的原理和自调用问题_方法调用_10

现在,CacheableService包含一个名为 的附加方法selfInvoke(),该方法未标记任何注释。该方法只是调用getFromCache(),这是一个用@Cacheable注释标记并在代理中增强以利用缓存的方法。

那么,问题就来了:该selfInvoke()方法会从缓存中检索值吗?

许多开发人员期望这应该可行并且缓存将被利用。然而,事实上,它并没有达到预期的效果。为了理解这一点,让我们分析一个包含 update CacheableService、其代理和方法调用的序列图selfInvoke()

Spring Boot注解:背后的原理和自调用问题_缓存_11

Spring Boot注解:背后的原理和自调用问题_拦截器_12编辑

所以,当我们调用selfInvoke()方法时,我们只访问getFromCache()目标bean中的方法。但是,为此方法启用缓存所做的所有增强都存在于getFromCache()代理内的重写方法中,不幸的是,该方法仍未被访问。

同样的问题也适用于其他注释,例如@Transactional@Retryable@Validated@Async等。

事实上,有多种选择可以解决这个问题。一种方法涉及重构代码以防止自调用标记有注释的方法并仅从其他 bean 调用它们。或者,可以考虑过渡到 AspectJ 来创建代理(编译时编织),而不是依赖 CGLib 和动态代理。此外,还可以采用一种称为自注入的技术,其中 Bean 被注入到其自身中,并在注入的 Bean 上调用目标方法。然而,值得注意的是,后一种解决方案本质上是一种解决方法。

结论

@Transactional在本文中,我们解释了 Spring 如何使用代理向标有、@Cacheable@Retryable@Validated@Async等注释的方法添加额外的逻辑。

我们还通过一些代码示例深入了解了这些注释在幕后如何工作。

但是,使用代理有时会导致自调用问题等问题。我们已经讨论了它是什么以及解决它的方法。

掌握这些概念很重要,我真的希望您觉得这篇文章很有趣。 

谢谢阅读!


【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读
  H7eKmxPQBBfu   2023年11月02日   50   0   0 缓存Redisspring
  3BoKag77WsNU   2023年11月02日   44   0   0 请求头缓存数据
  AQ5oXdIXiPb5   2023年11月02日   71   0   0 客户端缓存服务器
KMmKYbIfxJ2Q