在开发Spring Boot应用程序时,我们经常使用如@Transactional、@Cacheable、@Retryable、@Validated、@Async等注解。通过这些注解,我们为我们的bean添加了补充逻辑,如将数据库操作封装在事务中或实现缓存机制。
然而,并非每个人都会思考它们在底层是如何工作的,以及使用它们可能会出现什么问题。在这篇文章中,让我们开始一段旅程,探索最流行的注解是如何工作的,以及什么是自我调用问题。
Spring Beans和代理
当一些注解,如@Transactional,被应用到beans的方法中时。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,因为它
我们可以看到使用了 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); }
方法拦截器的操作如下:它检查缓存中是否有特定值,如果找到则直接返回该值。如果缓存中不存在该值,拦截器将通过invokeOperation(目标方法调用)协调其计算。计算完成后,拦截器将值存储在缓存中,然后返回它。
@Cacheable
在事务注释的情况下,代理使用另一个MethodInterceptor实现 - TransactionInterceptor:
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等注释的方法添加额外的逻辑。
我们还通过一些代码示例深入了解了这些注释在幕后如何工作。
但是,使用代理有时会导致自调用问题等问题。我们已经讨论了它是什么以及解决它的方法。
掌握这些概念很重要,我真的希望您觉得这篇文章很有趣。
谢谢阅读!
文章来源:https://www.toymoban.com/diary/share/297.html
文章来源地址https://www.toymoban.com/diary/share/297.html
到此这篇关于Spring Boot注解的幕后运作和自我调用问题的文章就介绍到这了,更多相关内容可以在右上角搜索或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!