From 038dda97f86f19df3857bb8154fa59bde97b66ce Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 18 Jul 2023 22:01:57 +0200 Subject: [PATCH 1/2] Document EntityManager injection via constructors/@Autowired Closes gh-15076 --- .../ROOT/pages/data-access/orm/jpa.adoc | 69 ++++++++++++++----- 1 file changed, 50 insertions(+), 19 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/data-access/orm/jpa.adoc b/framework-docs/modules/ROOT/pages/data-access/orm/jpa.adoc index 27d2feca599..b9fc4279fc3 100644 --- a/framework-docs/modules/ROOT/pages/data-access/orm/jpa.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/orm/jpa.adoc @@ -88,12 +88,6 @@ You can use this option for full JPA capabilities in a Spring-based application This includes web containers such as Tomcat, stand-alone applications, and integration tests with sophisticated persistence requirements. -NOTE: If you want to specifically configure a Hibernate setup, an immediate alternative -is to set up a native Hibernate `LocalSessionFactoryBean` instead of a plain JPA -`LocalContainerEntityManagerFactoryBean`, letting it interact with JPA access code -as well as native Hibernate access code. -See xref:data-access/orm/jpa.adoc#orm-jpa-hibernate[Native Hibernate setup for JPA interaction] for details. - The `LocalContainerEntityManagerFactoryBean` gives full control over `EntityManagerFactory` configuration and is appropriate for environments where fine-grained customization is required. The `LocalContainerEntityManagerFactoryBean` @@ -187,6 +181,7 @@ and automatic propagation of the weaver to all weaver-aware beans: [source,xml,indent=0,subs="verbatim,quotes"] ---- + ... @@ -425,20 +420,20 @@ Kotlin:: ---- ====== -The `@PersistenceContext` annotation has an optional attribute called `type`, which defaults to -`PersistenceContextType.TRANSACTION`. You can use this default to receive a shared +The `@PersistenceContext` annotation has an optional attribute called `type`, which defaults +to `PersistenceContextType.TRANSACTION`. You can use this default to receive a shared `EntityManager` proxy. The alternative, `PersistenceContextType.EXTENDED`, is a completely different affair. This results in a so-called extended `EntityManager`, which is not thread-safe and, hence, must not be used in a concurrently accessed component, such as a -Spring-managed singleton bean. Extended `EntityManager` instances are only supposed to be used# +Spring-managed singleton bean. Extended `EntityManager` instances are only supposed to be used in stateful components that, for example, reside in a session, with the lifecycle of the `EntityManager` not tied to a current transaction but rather being completely up to the application. .Method- and field-level Injection **** -You can apply annotations that indicate dependency injections (such as `@PersistenceUnit` and -`@PersistenceContext`) on field or methods inside a class -- hence the expressions +You can apply annotations that indicate dependency injections (such as `@PersistenceUnit` +and `@PersistenceContext`) on field or methods inside a class -- hence the expressions "`method-level injection`" and "`field-level injection`". Field-level annotations are concise and easier to use while method-level annotations allow for further processing of the injected dependency. In both cases, the member visibility (public, protected, or private) @@ -460,12 +455,53 @@ No import of any Spring class is required. Moreover, as the JPA annotations are the injections are applied automatically by the Spring container. This is appealing from a non-invasiveness perspective and can feel more natural to JPA developers. +[[orm-jpa-dao-autowired]] +=== Implementing DAOs Based on `@Autowired` (typically with constructor-based injection) + +`@PersistenceUnit` and `@PersistenceContext` can only be declared on methods and fields. +What about providing JPA resources via constructors and other `@Autowired` injection points? + +`EntityManagerFactory` can easily be injected via constructors and `@Autowired` fields/methods +as long as the target is defined as a bean, e.g. via `LocalContainerEntityManagerFactoryBean`. +The injection point matches the original `EntityManagerFactory` definition by type as-is. + +However, an `@PersistenceContext`-style shared `EntityManager` reference is not available for +regular dependency injection out of the box. In order to make it available for type-based +matching as required by `@Autowired`, consider defining a `SharedEntityManagerBean` as a +companion for your `EntityManagerFactory` definition: + +[source,xml,indent=0,subs="verbatim,quotes"] +---- + + ... + + + + + +---- + +Alternatively, you may define an `@Bean` method based on `SharedEntityManagerCreator`: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Bean("em") + public static EntityManager sharedEntityManager(EntityManagerFactory emf) { + return SharedEntityManagerCreator.createSharedEntityManager(emf); + } +---- + +In case of multiple persistence units, each `EntityManagerFactory` definition needs to be +accompanied by a corresponding `EntityManager` bean definition, ideally with qualifiers +that match with the distinct `EntityManagerFactory` definition in order to distinguish +the persistence units via `@Autowired @Qualifier("...")`. + [[orm-jpa-tx]] -== Spring-driven JPA transactions +== Spring-driven JPA Transactions -NOTE: We strongly encourage you to read xref:data-access/transaction/declarative.adoc[Declarative Transaction Management], if you have not -already done so, to get more detailed coverage of Spring's declarative transaction support. +NOTE: We strongly encourage you to read xref:data-access/transaction/declarative.adoc[Declarative Transaction Management], +if you have not already done so, to get more detailed coverage of Spring's declarative transaction support. The recommended strategy for JPA is local transactions through JPA's native transaction support. Spring's `JpaTransactionManager` provides many capabilities known from local @@ -478,11 +514,6 @@ to JDBC access code that accesses the same `DataSource`, provided that the regis Spring provides dialects for the EclipseLink and Hibernate JPA implementations. See the xref:data-access/orm/jpa.adoc#orm-jpa-dialect[next section] for details on the `JpaDialect` mechanism. -NOTE: As an immediate alternative, Spring's native `HibernateTransactionManager` is capable -of interacting with JPA access code, adapting to several Hibernate specifics and providing -JDBC interaction. This makes particular sense in combination with `LocalSessionFactoryBean` -setup. See xref:data-access/orm/jpa.adoc#orm-jpa-hibernate[Native Hibernate Setup for JPA Interaction] for details. - [[orm-jpa-dialect]] == Understanding `JpaDialect` and `JpaVendorAdapter` From bbcc788f609098f3dd22750718b446b122188ae1 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 18 Jul 2023 22:02:09 +0200 Subject: [PATCH 2/2] Decouple exception messages for sync=true from @Cacheable --- .../cache/annotation/Cacheable.java | 8 ++-- .../cache/interceptor/CacheAspectSupport.java | 35 +++++++++--------- .../interceptor/CacheSyncFailureTests.java | 37 ++++++++++--------- 3 files changed, 41 insertions(+), 39 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java b/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java index 51ea3ec2583..a207d1f0609 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -177,9 +177,9 @@ public @interface Cacheable { *
  • Only one cache may be specified
  • *
  • No other cache-related operation can be combined
  • * - * This is effectively a hint and the actual cache provider that you are - * using may not support it in a synchronized fashion. Check your provider - * documentation for more details on the actual semantics. + * This is effectively a hint and the chosen cache provider might not actually + * support it in a synchronized fashion. Check your provider documentation for + * more details on the actual semantics. * @since 4.3 * @see org.springframework.cache.Cache#get(Object, Callable) */ diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java index 572d3295db2..20bcb55dc6a 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java @@ -214,7 +214,7 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker @Override public void afterSingletonsInstantiated() { if (getCacheResolver() == null) { - // Lazily initialize cache resolver via default cache manager... + // Lazily initialize cache resolver via default cache manager Assert.state(this.beanFactory != null, "CacheResolver or BeanFactory must be set on cache aspect"); try { setCacheManager(this.beanFactory.getBean(CacheManager.class)); @@ -307,22 +307,22 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker } /** - * Return a bean with the specified name and type. Used to resolve services that - * are referenced by name in a {@link CacheOperation}. - * @param beanName the name of the bean, as defined by the operation - * @param expectedType type for the bean - * @return the bean matching that name + * Retrieve a bean with the specified name and type. + * Used to resolve services that are referenced by name in a {@link CacheOperation}. + * @param name the name of the bean, as defined by the cache operation + * @param serviceType the type expected by the operation's service reference + * @return the bean matching the expected type, qualified by the given name * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException if such bean does not exist * @see CacheOperation#getKeyGenerator() * @see CacheOperation#getCacheManager() * @see CacheOperation#getCacheResolver() */ - protected T getBean(String beanName, Class expectedType) { + protected T getBean(String name, Class serviceType) { if (this.beanFactory == null) { throw new IllegalStateException( - "BeanFactory must be set on cache aspect for " + expectedType.getSimpleName() + " retrieval"); + "BeanFactory must be set on cache aspect for " + serviceType.getSimpleName() + " retrieval"); } - return BeanFactoryAnnotationUtils.qualifiedBeanOfType(this.beanFactory, expectedType, beanName); + return BeanFactoryAnnotationUtils.qualifiedBeanOfType(this.beanFactory, serviceType, name); } /** @@ -388,12 +388,11 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker } } else { - // No caching required, only call the underlying method + // No caching required, just call the underlying method return invokeOperation(invoker); } } - // Process any early evictions processCacheEvicts(contexts.get(CacheEvictOperation.class), true, CacheOperationExpressionEvaluator.NO_RESULT); @@ -641,21 +640,21 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker if (syncEnabled) { if (this.contexts.size() > 1) { throw new IllegalStateException( - "@Cacheable(sync=true) cannot be combined with other cache operations on '" + method + "'"); + "A sync=true operation cannot be combined with other cache operations on '" + method + "'"); } if (cacheOperationContexts.size() > 1) { throw new IllegalStateException( - "Only one @Cacheable(sync=true) entry is allowed on '" + method + "'"); + "Only one sync=true operation is allowed on '" + method + "'"); } CacheOperationContext cacheOperationContext = cacheOperationContexts.iterator().next(); - CacheableOperation operation = (CacheableOperation) cacheOperationContext.getOperation(); + CacheOperation operation = cacheOperationContext.getOperation(); if (cacheOperationContext.getCaches().size() > 1) { throw new IllegalStateException( - "@Cacheable(sync=true) only allows a single cache on '" + operation + "'"); + "A sync=true operation is restricted to a single cache on '" + operation + "'"); } - if (StringUtils.hasText(operation.getUnless())) { + if (operation instanceof CacheableOperation cacheable && StringUtils.hasText(cacheable.getUnless())) { throw new IllegalStateException( - "@Cacheable(sync=true) does not support unless attribute on '" + operation + "'"); + "A sync=true operation does not support the unless attribute on '" + operation + "'"); } return true; } @@ -884,13 +883,13 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker } } + /** * Internal holder class for recording that a cache method was invoked. */ private static class InvocationAwareResult { boolean invoked; - } } diff --git a/spring-context/src/test/java/org/springframework/cache/interceptor/CacheSyncFailureTests.java b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheSyncFailureTests.java index de4776adae2..56b22970d1a 100644 --- a/spring-context/src/test/java/org/springframework/cache/interceptor/CacheSyncFailureTests.java +++ b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheSyncFailureTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,8 +48,9 @@ public class CacheSyncFailureTests { private SimpleService simpleService; + @BeforeEach - public void setUp() { + public void setup() { this.context = new AnnotationConfigApplicationContext(Config.class); this.simpleService = this.context.getBean(SimpleService.class); } @@ -61,39 +62,40 @@ public class CacheSyncFailureTests { } } + @Test public void unlessSync() { - assertThatIllegalStateException().isThrownBy(() -> - this.simpleService.unlessSync("key")) - .withMessageContaining("@Cacheable(sync=true) does not support unless attribute"); + assertThatIllegalStateException() + .isThrownBy(() -> this.simpleService.unlessSync("key")) + .withMessageContaining("A sync=true operation does not support the unless attribute"); } @Test public void severalCachesSync() { - assertThatIllegalStateException().isThrownBy(() -> - this.simpleService.severalCachesSync("key")) - .withMessageContaining("@Cacheable(sync=true) only allows a single cache"); + assertThatIllegalStateException() + .isThrownBy(() -> this.simpleService.severalCachesSync("key")) + .withMessageContaining("A sync=true operation is restricted to a single cache"); } @Test public void severalCachesWithResolvedSync() { - assertThatIllegalStateException().isThrownBy(() -> - this.simpleService.severalCachesWithResolvedSync("key")) - .withMessageContaining("@Cacheable(sync=true) only allows a single cache"); + assertThatIllegalStateException() + .isThrownBy(() -> this.simpleService.severalCachesWithResolvedSync("key")) + .withMessageContaining("A sync=true operation is restricted to a single cache"); } @Test public void syncWithAnotherOperation() { - assertThatIllegalStateException().isThrownBy(() -> - this.simpleService.syncWithAnotherOperation("key")) - .withMessageContaining("@Cacheable(sync=true) cannot be combined with other cache operations"); + assertThatIllegalStateException() + .isThrownBy(() -> this.simpleService.syncWithAnotherOperation("key")) + .withMessageContaining("A sync=true operation cannot be combined with other cache operations"); } @Test public void syncWithTwoGetOperations() { - assertThatIllegalStateException().isThrownBy(() -> - this.simpleService.syncWithTwoGetOperations("key")) - .withMessageContaining("Only one @Cacheable(sync=true) entry is allowed"); + assertThatIllegalStateException() + .isThrownBy(() -> this.simpleService.syncWithTwoGetOperations("key")) + .withMessageContaining("Only one sync=true operation is allowed"); } @@ -131,6 +133,7 @@ public class CacheSyncFailureTests { } } + @Configuration @EnableCaching static class Config implements CachingConfigurer {