diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/JpaEntityListenerTests.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/JpaEntityListenerTests.java
new file mode 100644
index 0000000000..b94e517b19
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/JpaEntityListenerTests.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright 2002-2022 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.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.junit.jupiter.orm;
+
+import java.util.List;
+
+import javax.persistence.EntityManager;
+import javax.persistence.EntityManagerFactory;
+import javax.persistence.PersistenceContext;
+import javax.sql.DataSource;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+import org.springframework.orm.jpa.JpaTransactionManager;
+import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
+import org.springframework.orm.jpa.vendor.Database;
+import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
+import org.springframework.test.context.jdbc.Sql;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+import org.springframework.test.context.junit.jupiter.orm.domain.JpaPersonRepository;
+import org.springframework.test.context.junit.jupiter.orm.domain.Person;
+import org.springframework.test.context.junit.jupiter.orm.domain.PersonListener;
+import org.springframework.test.context.junit.jupiter.orm.domain.PersonRepository;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+import org.springframework.transaction.annotation.Transactional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Transactional tests for JPA entity listener support (a.k.a. lifecycle callback
+ * methods).
+ *
+ * @author Sam Brannen
+ * @since 5.3.18
+ * @see issue gh-28228
+ * @see org.springframework.test.context.junit4.orm.HibernateSessionFlushingTests
+ */
+@SpringJUnitConfig
+@Transactional
+@Sql(statements = "insert into person(id, name) values(0, 'Jane')")
+class JpaEntityListenerTests {
+
+ @PersistenceContext
+ EntityManager entityManager;
+
+ @Autowired
+ JdbcTemplate jdbcTemplate;
+
+ @Autowired
+ PersonRepository repo;
+
+
+ @BeforeEach
+ void setUp() {
+ assertPeople("Jane");
+ PersonListener.methodsInvoked.clear();
+ }
+
+ @Test
+ void find() {
+ Person jane = repo.findByName("Jane");
+ assertCallbacks("@PostLoad: Jane");
+
+ // Does not cause an additional @PostLoad
+ repo.findById(jane.getId());
+ assertCallbacks("@PostLoad: Jane");
+
+ // Clear to cause a new @PostLoad
+ entityManager.clear();
+ repo.findById(jane.getId());
+ assertCallbacks("@PostLoad: Jane", "@PostLoad: Jane");
+ }
+
+ @Test
+ void save() {
+ Person john = repo.save(new Person("John"));
+ assertCallbacks("@PrePersist: John");
+
+ // Flush to cause a @PostPersist
+ entityManager.flush();
+ assertPeople("Jane", "John");
+ assertCallbacks("@PrePersist: John", "@PostPersist: John");
+
+ // Does not cause a @PostLoad
+ repo.findById(john.getId());
+ assertCallbacks("@PrePersist: John", "@PostPersist: John");
+
+ // Clear to cause a @PostLoad
+ entityManager.clear();
+ repo.findById(john.getId());
+ assertCallbacks("@PrePersist: John", "@PostPersist: John", "@PostLoad: John");
+ }
+
+ @Test
+ void update() {
+ Person jane = repo.findByName("Jane");
+ assertCallbacks("@PostLoad: Jane");
+
+ jane.setName("Jane Doe");
+ // Does not cause a @PreUpdate or @PostUpdate
+ repo.save(jane);
+ assertCallbacks("@PostLoad: Jane");
+
+ // Flush to cause a @PreUpdate and @PostUpdate
+ entityManager.flush();
+ assertPeople("Jane Doe");
+ assertCallbacks("@PostLoad: Jane", "@PreUpdate: Jane Doe", "@PostUpdate: Jane Doe");
+ }
+
+ @Test
+ void remove() {
+ Person jane = repo.findByName("Jane");
+ assertCallbacks("@PostLoad: Jane");
+
+ // Does not cause a @PostRemove
+ repo.remove(jane);
+ assertCallbacks("@PostLoad: Jane", "@PreRemove: Jane");
+
+ // Flush to cause a @PostRemove
+ entityManager.flush();
+ assertPeople();
+ assertCallbacks("@PostLoad: Jane", "@PreRemove: Jane", "@PostRemove: Jane");
+ }
+
+ private void assertCallbacks(String... callbacks) {
+ assertThat(PersonListener.methodsInvoked).containsExactly(callbacks);
+ }
+
+ private void assertPeople(String... expectedNames) {
+ List names = this.jdbcTemplate.queryForList("select name from person", String.class);
+ if (expectedNames.length == 0) {
+ assertThat(names).isEmpty();
+ }
+ else {
+ assertThat(names).containsExactlyInAnyOrder(expectedNames);
+ }
+ }
+
+
+ @Configuration(proxyBeanMethods = false)
+ @EnableTransactionManagement
+ static class Config {
+
+ @Bean
+ PersonRepository personRepository() {
+ return new JpaPersonRepository();
+ }
+
+ @Bean
+ DataSource dataSource() {
+ return new EmbeddedDatabaseBuilder().generateUniqueName(true).build();
+ }
+
+ @Bean
+ JdbcTemplate jdbcTemplate(DataSource dataSource) {
+ return new JdbcTemplate(dataSource);
+ }
+
+ @Bean
+ LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
+ LocalContainerEntityManagerFactoryBean emfb = new LocalContainerEntityManagerFactoryBean();
+ emfb.setDataSource(dataSource);
+ emfb.setPackagesToScan(Person.class.getPackage().getName());
+ HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
+ hibernateJpaVendorAdapter.setGenerateDdl(true);
+ hibernateJpaVendorAdapter.setDatabase(Database.HSQL);
+ emfb.setJpaVendorAdapter(hibernateJpaVendorAdapter);
+ return emfb;
+ }
+
+ @Bean
+ JpaTransactionManager transactionManager(EntityManagerFactory emf) {
+ return new JpaTransactionManager(emf);
+ }
+
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/JpaPersonRepository.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/JpaPersonRepository.java
new file mode 100644
index 0000000000..a3984b5359
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/JpaPersonRepository.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2002-2022 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.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.junit.jupiter.orm.domain;
+
+import javax.persistence.EntityManager;
+import javax.persistence.PersistenceContext;
+
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * JPA based implementation of the {@link PersonRepository} API.
+ *
+ * @author Sam Brannen
+ * @since 5.3.18
+ */
+@Transactional
+@Repository
+public class JpaPersonRepository implements PersonRepository {
+
+ @PersistenceContext
+ private EntityManager entityManager;
+
+ @Override
+ public Person findById(Long id) {
+ return this.entityManager.find(Person.class, id);
+ }
+
+ @Override
+ public Person findByName(String name) {
+ return this.entityManager.createQuery("from Person where name = :name", Person.class)
+ .setParameter("name", name)
+ .getSingleResult();
+ }
+
+ @Override
+ public Person save(Person person) {
+ this.entityManager.persist(person);
+ return person;
+ }
+
+ @Override
+ public void remove(Person person) {
+ this.entityManager.remove(person);
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/Person.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/Person.java
new file mode 100644
index 0000000000..8d8f5b22fb
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/Person.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2002-2022 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.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.junit.jupiter.orm.domain;
+
+import javax.persistence.Entity;
+import javax.persistence.EntityListeners;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+
+/**
+ * Person entity.
+ *
+ * @author Sam Brannen
+ * @since 5.3.18
+ */
+@Entity
+@EntityListeners(PersonListener.class)
+public class Person {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ private Long id;
+
+ private String name;
+
+
+ public Person() {
+ }
+
+ public Person(String name) {
+ this.name = name;
+ }
+
+ public Long getId() {
+ return this.id;
+ }
+
+ protected void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/PersonListener.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/PersonListener.java
new file mode 100644
index 0000000000..1b5c56ddf6
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/PersonListener.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2002-2022 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.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.junit.jupiter.orm.domain;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.persistence.PostLoad;
+import javax.persistence.PostPersist;
+import javax.persistence.PostRemove;
+import javax.persistence.PostUpdate;
+import javax.persistence.PrePersist;
+import javax.persistence.PreRemove;
+import javax.persistence.PreUpdate;
+
+/**
+ * Person entity listener.
+ *
+ * @author Sam Brannen
+ * @since 5.3.18
+ */
+public class PersonListener {
+
+ public static final List methodsInvoked = new ArrayList<>();
+
+
+ @PostLoad
+ public void postLoad(Person person) {
+ methodsInvoked.add("@PostLoad: " + person.getName());
+ }
+
+ @PrePersist
+ public void prePersist(Person person) {
+ methodsInvoked.add("@PrePersist: " + person.getName());
+ }
+
+ @PostPersist
+ public void postPersist(Person person) {
+ methodsInvoked.add("@PostPersist: " + person.getName());
+ }
+
+ @PreUpdate
+ public void preUpdate(Person person) {
+ methodsInvoked.add("@PreUpdate: " + person.getName());
+ }
+
+ @PostUpdate
+ public void postUpdate(Person person) {
+ methodsInvoked.add("@PostUpdate: " + person.getName());
+ }
+
+ @PreRemove
+ public void preRemove(Person person) {
+ methodsInvoked.add("@PreRemove: " + person.getName());
+ }
+
+ @PostRemove
+ public void postRemove(Person person) {
+ methodsInvoked.add("@PostRemove: " + person.getName());
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/PersonRepository.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/PersonRepository.java
new file mode 100644
index 0000000000..9576a649d0
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/domain/PersonRepository.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2002-2022 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.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.junit.jupiter.orm.domain;
+
+/**
+ * Person repository API.
+ *
+ * @author Sam Brannen
+ * @since 5.3.18
+ */
+public interface PersonRepository {
+
+ Person findById(Long id);
+
+ Person findByName(String name);
+
+ Person save(Person person);
+
+ void remove(Person person);
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/orm/HibernateSessionFlushingTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/orm/HibernateSessionFlushingTests.java
index 6c0dc92e90..e959681cc3 100644
--- a/spring-test/src/test/java/org/springframework/test/context/junit4/orm/HibernateSessionFlushingTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/junit4/orm/HibernateSessionFlushingTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2022 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.
@@ -44,6 +44,7 @@ import static org.springframework.test.transaction.TransactionAssert.assertThatT
* @author Juergen Hoeller
* @author Vlad Mihalcea
* @since 3.0
+ * @see org.springframework.test.context.junit.jupiter.orm.JpaEntityListenerTests
*/
@ContextConfiguration
public class HibernateSessionFlushingTests extends AbstractTransactionalJUnit4SpringContextTests {