From 92cd3338549d897674afb409c5e49a8a105be746 Mon Sep 17 00:00:00 2001 From: Kazuki Shimizu Date: Sat, 14 Apr 2018 16:25:54 +0900 Subject: [PATCH] DATAJDBC-204 - Support for annotation based auditing. When enabled by using @EnableJdbcAuditing entities will get properties annotated with * @CreatedBy * @CreatedDate * @LastModifiedBy * @LastModifiedDate updated when saved. Original pull request: #64. --- .../support/JdbcAuditingEventListener.java | 74 ++++++ .../repository/config/EnableJdbcAuditing.java | 90 +++++++ .../config/JdbcAuditingRegistrar.java | 83 +++++++ ...nableJdbcAuditingHsqlIntegrationTests.java | 230 ++++++++++++++++++ ...nableJdbcRepositoriesIntegrationTests.java | 3 +- ...eJdbcAuditingHsqlIntegrationTests-hsql.sql | 8 + 6 files changed, 487 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/springframework/data/jdbc/domain/support/JdbcAuditingEventListener.java create mode 100644 src/main/java/org/springframework/data/jdbc/repository/config/EnableJdbcAuditing.java create mode 100644 src/main/java/org/springframework/data/jdbc/repository/config/JdbcAuditingRegistrar.java create mode 100644 src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcAuditingHsqlIntegrationTests.java create mode 100644 src/test/resources/org.springframework.data.jdbc.repository.config/EnableJdbcAuditingHsqlIntegrationTests-hsql.sql diff --git a/src/main/java/org/springframework/data/jdbc/domain/support/JdbcAuditingEventListener.java b/src/main/java/org/springframework/data/jdbc/domain/support/JdbcAuditingEventListener.java new file mode 100644 index 00000000..f500e9b6 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/domain/support/JdbcAuditingEventListener.java @@ -0,0 +1,74 @@ +/* + * Copyright 2018 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 + * + * http://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.data.jdbc.domain.support; + +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.context.ApplicationListener; +import org.springframework.data.auditing.AuditingHandler; +import org.springframework.data.jdbc.mapping.event.BeforeSaveEvent; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Spring JDBC event listener to capture auditing information on persisting and updating entities. + *

+ * You can enable this class just a matter of activating auditing using {@link org.springframework.data.jdbc.repository.config.EnableJdbcAuditing} in your Spring config: + * + *

+ * @Configuration
+ * @EnableJdbcRepositories
+ * @EnableJdbcAuditing
+ * class JdbcRepositoryConfig {
+ * }
+ * 
+ * + * @author Kazuki Shimizu + * @see org.springframework.data.jdbc.repository.config.EnableJdbcAuditing + * @since 1.0 + */ +public class JdbcAuditingEventListener implements ApplicationListener { + + @Nullable + private AuditingHandler handler; + + /** + * Configures the {@link AuditingHandler} to be used to set the current auditor on the domain types touched. + * + * @param auditingHandler must not be {@literal null}. + */ + public void setAuditingHandler(ObjectFactory auditingHandler) { + Assert.notNull(auditingHandler, "AuditingHandler must not be null!"); + this.handler = auditingHandler.getObject(); + } + + /** + * {@inheritDoc} + * @param event a notification event for indicating before save + */ + @Override + public void onApplicationEvent(BeforeSaveEvent event) { + if (handler != null) { + event.getOptionalEntity().ifPresent(entity -> { + if (event.getId().getOptionalValue().isPresent()) { + handler.markModified(entity); + } else { + handler.markCreated(entity); + } + }); + } + } + +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/config/EnableJdbcAuditing.java b/src/main/java/org/springframework/data/jdbc/repository/config/EnableJdbcAuditing.java new file mode 100644 index 00000000..172397c1 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/config/EnableJdbcAuditing.java @@ -0,0 +1,90 @@ +/* + * Copyright 2018 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 + * + * http://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.data.jdbc.repository.config; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Import; +import org.springframework.data.auditing.DateTimeProvider; +import org.springframework.data.domain.AuditorAware; + +/** + * Annotation to enable auditing in JDBC via annotation configuration. + * + * If you use the auditing feature, you should be configures beans of Spring Data JDBC + * using {@link org.springframework.data.jdbc.repository.config.EnableJdbcRepositories} in your Spring config: + * + *
+ * @Configuration
+ * @EnableJdbcRepositories
+ * @EnableJdbcAuditing
+ * class JdbcRepositoryConfig {
+ * }
+ * 
+ * + *

+ * Note: This feature cannot use to a entity that implements {@link org.springframework.data.domain.Auditable} + * because the Spring Data JDBC does not support an {@link java.util.Optional} property yet. + *

+ * + * @see EnableJdbcRepositories + * @author Kazuki Shimizu + * @since 1.0 + */ +@Inherited +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Import(JdbcAuditingRegistrar.class) +public @interface EnableJdbcAuditing { + + /** + * Configures the {@link AuditorAware} bean to be used to lookup the current principal. + * + * @return + * @see AuditorAware + */ + String auditorAwareRef() default ""; + + /** + * Configures whether the creation and modification dates are set. + * + * @return + */ + boolean setDates() default true; + + /** + * Configures whether the entity shall be marked as modified on creation. + * + * @return + */ + boolean modifyOnCreate() default true; + + /** + * Configures a {@link DateTimeProvider} bean name that allows customizing the {@link java.time.LocalDateTime} to be + * used for setting creation and modification dates. + * + * @return + * @see DateTimeProvider + */ + String dateTimeProviderRef() default ""; + +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/config/JdbcAuditingRegistrar.java b/src/main/java/org/springframework/data/jdbc/repository/config/JdbcAuditingRegistrar.java new file mode 100644 index 00000000..a734592f --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/config/JdbcAuditingRegistrar.java @@ -0,0 +1,83 @@ +/* + * Copyright 2018 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 + * + * http://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.data.jdbc.repository.config; + +import java.lang.annotation.Annotation; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport; +import org.springframework.data.auditing.config.AuditingConfiguration; +import org.springframework.data.config.ParsingUtils; +import org.springframework.data.jdbc.domain.support.JdbcAuditingEventListener; + +/** + * {@link ImportBeanDefinitionRegistrar} to enable {@link EnableJdbcAuditing} annotation. + * + * @see EnableJdbcAuditing + * @author Kazuki Shimizu + * @since 1.0 + */ +class JdbcAuditingRegistrar extends AuditingBeanDefinitionRegistrarSupport { + + /** + * {@inheritDoc} + * @return return the {@link EnableJdbcAuditing} + * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#getAnnotation() + */ + @Override + protected Class getAnnotation() { + return EnableJdbcAuditing.class; + } + + /** + * {@inheritDoc} + * @return return "{@literal jdbcAuditingHandler}" + * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#getAuditingHandlerBeanName() + */ + @Override + protected String getAuditingHandlerBeanName() { + return "jdbcAuditingHandler"; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#getAuditHandlerBeanDefinitionBuilder(org.springframework.data.auditing.config.AuditingConfiguration) + */ + @Override + protected BeanDefinitionBuilder getAuditHandlerBeanDefinitionBuilder(AuditingConfiguration configuration) { + BeanDefinitionBuilder builder = super.getAuditHandlerBeanDefinitionBuilder(configuration); + return builder.addConstructorArgReference("jdbcMappingContext"); + } + + /** + * Register the bean definition of {@link JdbcAuditingEventListener}. + * {@inheritDoc} + * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#registerAuditListenerBeanDefinition(BeanDefinition, BeanDefinitionRegistry) + */ + @Override + protected void registerAuditListenerBeanDefinition(BeanDefinition auditingHandlerDefinition, + BeanDefinitionRegistry registry) { + Class listenerClass = JdbcAuditingEventListener.class; + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(listenerClass); + builder.addPropertyValue("auditingHandler", + ParsingUtils.getObjectFactoryBeanDefinition(getAuditingHandlerBeanName(), null)); + registerInfrastructureBeanWithId(builder.getRawBeanDefinition(), listenerClass.getName(), registry); + } + +} diff --git a/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcAuditingHsqlIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcAuditingHsqlIntegrationTests.java new file mode 100644 index 00000000..e685cc55 --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcAuditingHsqlIntegrationTests.java @@ -0,0 +1,230 @@ +/* + * Copyright 2018 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 + * + * http://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.data.jdbc.repository.config; + +import lombok.Data; +import org.junit.Test; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Primary; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.auditing.DateTimeProvider; +import org.springframework.data.domain.AuditorAware; +import org.springframework.data.jdbc.mapping.model.NamingStrategy; +import org.springframework.data.repository.CrudRepository; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests the {@link EnableJdbcAuditing} annotation. + * + * @author Kazuki Shimizu + */ +public class EnableJdbcAuditingHsqlIntegrationTests { + + @Test + public void auditForAnnotatedEntity() throws InterruptedException { + try (ConfigurableApplicationContext context = + new AnnotationConfigApplicationContext(TestConfiguration.class, AuditingConfiguration.class)) { + + AuditingAnnotatedDummyEntityRepository repository = context.getBean(AuditingAnnotatedDummyEntityRepository.class); + + AuditingConfiguration.currentAuditor = "user01"; + LocalDateTime now = LocalDateTime.now(); + + AuditingAnnotatedDummyEntity entity = new AuditingAnnotatedDummyEntity(); + entity.setName("Spring Data"); + repository.save(entity); + + assertThat(entity.id).isNotNull(); + assertThat(entity.getCreatedBy()).isEqualTo("user01"); + assertThat(entity.getCreatedDate()).isAfter(now); + assertThat(entity.getLastModifiedBy()).isEqualTo("user01"); + assertThat(entity.getLastModifiedDate()).isAfterOrEqualTo(entity.getCreatedDate()); + assertThat(entity.getLastModifiedDate()).isAfter(now); + assertThat(repository.findById(entity.id).get()).isEqualTo(entity); + + LocalDateTime beforeCreatedDate = entity.getCreatedDate(); + LocalDateTime beforeLastModifiedDate = entity.getLastModifiedDate(); + + TimeUnit.MILLISECONDS.sleep(100); + AuditingConfiguration.currentAuditor = "user02"; + + entity.setName("Spring Data JDBC"); + repository.save(entity); + + assertThat(entity.getCreatedBy()).isEqualTo("user01"); + assertThat(entity.getCreatedDate()).isEqualTo(beforeCreatedDate); + assertThat(entity.getLastModifiedBy()).isEqualTo("user02"); + assertThat(entity.getLastModifiedDate()).isAfter(beforeLastModifiedDate); + assertThat(repository.findById(entity.id).get()).isEqualTo(entity); + } + } + + @Test + public void noAnnotatedEntity() { + try (ConfigurableApplicationContext context = + new AnnotationConfigApplicationContext(TestConfiguration.class, AuditingConfiguration.class)) { + + DummyEntityRepository repository = context.getBean(DummyEntityRepository.class); + + DummyEntity entity = new DummyEntity(); + entity.setName("Spring Data"); + repository.save(entity); + + assertThat(entity.id).isNotNull(); + assertThat(repository.findById(entity.id).get()).isEqualTo(entity); + + entity.setName("Spring Data JDBC"); + repository.save(entity); + + assertThat(repository.findById(entity.id).get()).isEqualTo(entity); + } + } + + @Test + public void customizeEnableJdbcAuditingAttributes() { + // Test for 'auditorAwareRef', 'dateTimeProviderRef' and 'modifyOnCreate' + try (ConfigurableApplicationContext context = + new AnnotationConfigApplicationContext(TestConfiguration.class, CustomizeAuditingConfiguration1.class)) { + AuditingAnnotatedDummyEntityRepository repository = context.getBean(AuditingAnnotatedDummyEntityRepository.class); + + LocalDateTime currentDateTime = LocalDate.of(2018, 4, 14).atStartOfDay(); + CustomizeAuditingConfiguration1.currentDateTime = currentDateTime; + + AuditingAnnotatedDummyEntity entity = new AuditingAnnotatedDummyEntity(); + entity.setName("Spring Data JDBC"); + repository.save(entity); + + assertThat(entity.id).isNotNull(); + assertThat(entity.getCreatedBy()).isEqualTo("custom user"); + assertThat(entity.getCreatedDate()).isEqualTo(currentDateTime); + assertThat(entity.getLastModifiedBy()).isNull(); + assertThat(entity.getLastModifiedDate()).isNull(); + } + // Test for 'setDates' + try (ConfigurableApplicationContext context = + new AnnotationConfigApplicationContext(TestConfiguration.class, CustomizeAuditingConfiguration2.class)) { + AuditingAnnotatedDummyEntityRepository repository = context.getBean(AuditingAnnotatedDummyEntityRepository.class); + + AuditingAnnotatedDummyEntity entity = new AuditingAnnotatedDummyEntity(); + entity.setName("Spring Data JDBC"); + repository.save(entity); + + assertThat(entity.id).isNotNull(); + assertThat(entity.getCreatedBy()).isEqualTo("user"); + assertThat(entity.getCreatedDate()).isNull(); + assertThat(entity.getLastModifiedBy()).isEqualTo("user"); + assertThat(entity.getLastModifiedDate()).isNull(); + } + } + + + interface AuditingAnnotatedDummyEntityRepository extends CrudRepository { + } + + @Data + static class AuditingAnnotatedDummyEntity { + @Id + private Long id; + private String name; + @CreatedBy + private String createdBy; + @CreatedDate + private LocalDateTime createdDate; + @LastModifiedBy + private String lastModifiedBy; + @LastModifiedDate + private LocalDateTime lastModifiedDate; + } + + interface DummyEntityRepository extends CrudRepository { + } + + @Data + static class DummyEntity { + @Id + private Long id; + private String name; + } + + @ComponentScan("org.springframework.data.jdbc.testing") + @EnableJdbcRepositories(considerNestedRepositories = true) + static class TestConfiguration { + + @Bean + Class testClass() { + return EnableJdbcAuditingHsqlIntegrationTests.class; + } + + @Bean + NamingStrategy namingStrategy() { + return new NamingStrategy() { + public String getTableName(Class type) { + return "DummyEntity"; + } + }; + } + + } + + @EnableJdbcAuditing + static class AuditingConfiguration { + private static String currentAuditor; + @Bean + AuditorAware auditorAware() { + return () -> Optional.ofNullable(currentAuditor); + } + } + + @EnableJdbcAuditing(auditorAwareRef = "customAuditorAware", dateTimeProviderRef = "customDateTimeProvider", modifyOnCreate = false) + static class CustomizeAuditingConfiguration1 { + private static LocalDateTime currentDateTime; + @Bean + @Primary + AuditorAware auditorAware() { + return () -> Optional.of("default user"); + } + @Bean + AuditorAware customAuditorAware() { + return () -> Optional.of("custom user"); + } + @Bean + DateTimeProvider customDateTimeProvider() { + return () -> Optional.ofNullable(currentDateTime); + } + } + + @EnableJdbcAuditing(setDates = false) + static class CustomizeAuditingConfiguration2 { + @Bean + AuditorAware auditorAware() { + return () -> Optional.of("user"); + } + } + +} diff --git a/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java index 9b0edde4..abcd128c 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java +++ b/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java @@ -28,6 +28,7 @@ import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.repository.RowMapperMap; import org.springframework.data.jdbc.repository.config.EnableJdbcRepositoriesIntegrationTests.TestConfiguration; @@ -89,7 +90,7 @@ public class EnableJdbcRepositoriesIntegrationTests { } @ComponentScan("org.springframework.data.jdbc.testing") - @EnableJdbcRepositories(considerNestedRepositories = true) + @EnableJdbcRepositories(considerNestedRepositories = true, includeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = DummyRepository.class)) static class TestConfiguration { @Bean diff --git a/src/test/resources/org.springframework.data.jdbc.repository.config/EnableJdbcAuditingHsqlIntegrationTests-hsql.sql b/src/test/resources/org.springframework.data.jdbc.repository.config/EnableJdbcAuditingHsqlIntegrationTests-hsql.sql new file mode 100644 index 00000000..fc86c5cf --- /dev/null +++ b/src/test/resources/org.springframework.data.jdbc.repository.config/EnableJdbcAuditingHsqlIntegrationTests-hsql.sql @@ -0,0 +1,8 @@ +CREATE TABLE DummyEntity ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, + name VARCHAR(128), + createdBy VARCHAR(128), + createdDate TIMESTAMP, + lastModifiedBy VARCHAR(128), + lastModifiedDate TIMESTAMP +); \ No newline at end of file